diff --git a/README.md b/README.md index 143a897..8aa8bfa 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Getting Started +## Getting Started > 声明:此项目只发布于 Github,基于 MIT 协议,免费且作为开源学习使用。并且不会有任何形式的卖号等行为,谨防受骗。 @@ -20,19 +20,19 @@ #### 初始化 -* 初始化数据库 +初始化数据库 - * 创建数据库aideepin - * 执行docs/create.sql - * 填充openai的secret_key +* 创建数据库aideepin +* 执行docs/create.sql +* 填充openai的secret\_key -``` +```plaintext update adi_sys_config set value = 'my_chatgpt_secret_key' where name = 'secret_key' ``` * 修改配置文件 - * mysql: application-[dev|prod].xml中的spring.datasource + * postgresql: application-[dev|prod].xml中的spring.datasource * redis: application-[dev|prod].xml中的spring.data.redis * mail: application.xml中的spring.mail @@ -40,7 +40,7 @@ update adi_sys_config set value = 'my_chatgpt_secret_key' where name = 'secret_k * 进入项目 -``` +```plaintext cd aideepin ``` @@ -50,18 +50,18 @@ cd aideepin mvn clean package -Dmaven.test.skip=true ``` -* 运行 +* 运行 a. jar包启动: -``` +```plaintext cd adi-chat/target nohup java -jar -Xms768m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError adi-chat-0.0.1-SNAPSHOT.jar --spring.profiles.active=[dev|prod] dev/null 2>&1 & ``` b. docker启动 -``` +```plaintext cd adi-chat docker build . -t aideepin:0.0.1 docker run -d \ @@ -69,4 +69,4 @@ docker run -d \ -e APP_PROFILE=[dev|prod] \ -v="/data/aideepin/logs:/data/logs" \ aideepin:0.0.1 -``` +``` \ No newline at end of file diff --git a/adi-admin/pom.xml b/adi-admin/pom.xml index 6ac206e..f3c73a1 100644 --- a/adi-admin/pom.xml +++ b/adi-admin/pom.xml @@ -11,18 +11,7 @@ adi-admin - - 17 - 17 - UTF-8 - - - - com.moyz - adi-bootstrap - 0.0.1-SNAPSHOT - com.moyz adi-common diff --git a/adi-admin/src/main/java/com/moyz/adi/admin/controller/SystemConfigController.java b/adi-admin/src/main/java/com/moyz/adi/admin/controller/SystemConfigController.java new file mode 100644 index 0000000..5395a50 --- /dev/null +++ b/adi-admin/src/main/java/com/moyz/adi/admin/controller/SystemConfigController.java @@ -0,0 +1,25 @@ +package com.moyz.adi.admin.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.moyz.adi.common.entity.SysConfig; +import com.moyz.adi.common.service.KnowledgeBaseService; +import com.moyz.adi.common.service.SysConfigService; +import jakarta.annotation.Resource; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/admin/sys-config") +@Validated +public class SystemConfigController { + + @Resource + private SysConfigService sysConfigService; + + public Page list(String keyword, @NotNull @Min(1) Integer currentPage, @NotNull @Min(10) Integer pageSize) { + return sysConfigService.search(keyword, currentPage, pageSize); + } +} diff --git a/adi-bootstrap/pom.xml b/adi-bootstrap/pom.xml index b4e9695..e2aeac7 100644 --- a/adi-bootstrap/pom.xml +++ b/adi-bootstrap/pom.xml @@ -11,6 +11,18 @@ adi-bootstrap + + + com.moyz + adi-chat + 0.0.1-SNAPSHOT + + + com.moyz + adi-admin + 0.0.1-SNAPSHOT + + diff --git a/adi-bootstrap/src/main/resources/application-dev.yml b/adi-bootstrap/src/main/resources/application-dev.yml index a61b950..420c93c 100644 --- a/adi-bootstrap/src/main/resources/application-dev.yml +++ b/adi-bootstrap/src/main/resources/application-dev.yml @@ -1,8 +1,8 @@ spring: datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/aideepin?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&tinyInt1isBit=false&allowMultiQueries=true - username: root + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://172.17.18.164:5432/aideepin2?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&tinyInt1isBit=false&allowMultiQueries=true + username: postgres password: 123456 data: redis: diff --git a/adi-bootstrap/src/main/resources/application-prod.yml b/adi-bootstrap/src/main/resources/application-prod.yml index 3eaba77..36dbf60 100644 --- a/adi-bootstrap/src/main/resources/application-prod.yml +++ b/adi-bootstrap/src/main/resources/application-prod.yml @@ -1,9 +1,9 @@ spring: datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/aideepin?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&tinyInt1isBit=false&allowMultiQueries=true - username: your-mysql-account - password: your-mysql-password + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://localhost:3306/aideepin?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&tinyInt1isBit=false&allowMultiQueries=true + username: your-db-account + password: your-db-password data: redis: host: localhost diff --git a/adi-chat/pom.xml b/adi-chat/pom.xml index 8fe63ac..a6d3f86 100644 --- a/adi-chat/pom.xml +++ b/adi-chat/pom.xml @@ -12,11 +12,6 @@ adi-chat - - com.moyz - adi-bootstrap - 0.0.1-SNAPSHOT - com.moyz adi-common diff --git a/adi-chat/src/main/java/com/moyz/adi/chat/controller/FileController.java b/adi-chat/src/main/java/com/moyz/adi/chat/controller/FileController.java index d148e6e..70d608f 100644 --- a/adi-chat/src/main/java/com/moyz/adi/chat/controller/FileController.java +++ b/adi-chat/src/main/java/com/moyz/adi/chat/controller/FileController.java @@ -38,7 +38,7 @@ public class FileController { @PostMapping(path = "/file/upload", headers = "content-type=multipart/form-data", produces = MediaType.APPLICATION_JSON_VALUE) public Map upload(@RequestPart(value = "file") MultipartFile file) { Map result = new HashMap<>(); - result.put("uuid", fileService.writeToLocal(file)); + result.put("uuid", fileService.writeToLocal(file).getUuid()); return result; } diff --git a/adi-chat/src/main/java/com/moyz/adi/chat/controller/KnowledgeBaseController.java b/adi-chat/src/main/java/com/moyz/adi/chat/controller/KnowledgeBaseController.java new file mode 100644 index 0000000..2c131ae --- /dev/null +++ b/adi-chat/src/main/java/com/moyz/adi/chat/controller/KnowledgeBaseController.java @@ -0,0 +1,62 @@ +package com.moyz.adi.chat.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.moyz.adi.common.dto.KbEditReq; +import com.moyz.adi.common.entity.AdiFile; +import com.moyz.adi.common.entity.KnowledgeBase; +import com.moyz.adi.common.service.KnowledgeBaseService; +import jakarta.annotation.Resource; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping("/knowledge-base") +@Validated +public class KnowledgeBaseController { + + @Resource + private KnowledgeBaseService knowledgeBaseService; + + @PostMapping("/saveOrUpdate") + public KnowledgeBase saveOrUpdate(@RequestBody KbEditReq kbEditReq) { + return knowledgeBaseService.saveOrUpdate(kbEditReq); + } + + @PostMapping(path = "/uploadDocs/{uuid}", headers = "content-type=multipart/form-data", produces = MediaType.APPLICATION_JSON_VALUE) + public boolean uploadDocs(@PathVariable String uuid,@RequestParam(value = "embedding", defaultValue = "true") Boolean embedding, @RequestParam("files") MultipartFile[] docs) { + knowledgeBaseService.uploadDocs(uuid, embedding, docs); + return true; + } + + @PostMapping(path = "/upload/{uuid}", headers = "content-type=multipart/form-data", produces = MediaType.APPLICATION_JSON_VALUE) + public AdiFile upload(@PathVariable String uuid, @RequestParam(value = "embedding", defaultValue = "true") Boolean embedding, @RequestParam("file") MultipartFile doc) { + return knowledgeBaseService.uploadDoc(uuid, embedding, doc); + } + + @GetMapping("/search") + public Page list(String keyword, Boolean includeOthersPublic, @NotNull @Min(1) Integer currentPage, @NotNull @Min(10) Integer pageSize) { + return knowledgeBaseService.search(keyword, includeOthersPublic, currentPage, pageSize); + } + + @GetMapping("/info/{uuid}") + public KnowledgeBase info(@PathVariable String uuid) { + return knowledgeBaseService.lambdaQuery() + .eq(KnowledgeBase::getUuid, uuid) + .eq(KnowledgeBase::getIsDeleted, false) + .one(); + } + + @PostMapping("/del/{uuid}") + public boolean softDelete(@PathVariable String uuid) { + return knowledgeBaseService.softDelete(uuid); + } + + @PostMapping("/embedding/{uuid}") + public boolean embedding(@PathVariable String uuid, @RequestParam(defaultValue = "false") Boolean forceAll) { + return knowledgeBaseService.embedding(uuid, forceAll); + } +} diff --git a/adi-chat/src/main/java/com/moyz/adi/chat/controller/KnowledgeBaseEmbeddingController.java b/adi-chat/src/main/java/com/moyz/adi/chat/controller/KnowledgeBaseEmbeddingController.java new file mode 100644 index 0000000..134e803 --- /dev/null +++ b/adi-chat/src/main/java/com/moyz/adi/chat/controller/KnowledgeBaseEmbeddingController.java @@ -0,0 +1,25 @@ +package com.moyz.adi.chat.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.moyz.adi.common.dto.KbItemEmbeddingDto; +import com.moyz.adi.common.service.KnowledgeBaseEmbeddingService; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/knowledge-base-embedding") +@Validated +public class KnowledgeBaseEmbeddingController { + + @Resource + private KnowledgeBaseEmbeddingService knowledgeBaseEmbeddingService; + + @GetMapping("/list/{kbItemUuid}") + public Page list(@PathVariable String kbItemUuid, int currentPage, int pageSize) { + return knowledgeBaseEmbeddingService.listByItemUuid(kbItemUuid, currentPage, pageSize); + } +} diff --git a/adi-chat/src/main/java/com/moyz/adi/chat/controller/KnowledgeBaseItemController.java b/adi-chat/src/main/java/com/moyz/adi/chat/controller/KnowledgeBaseItemController.java new file mode 100644 index 0000000..560d763 --- /dev/null +++ b/adi-chat/src/main/java/com/moyz/adi/chat/controller/KnowledgeBaseItemController.java @@ -0,0 +1,56 @@ +package com.moyz.adi.chat.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.moyz.adi.common.dto.KbItemEditReq; +import com.moyz.adi.common.dto.KbItemEmbeddingBatchReq; +import com.moyz.adi.common.entity.KnowledgeBaseItem; +import com.moyz.adi.common.service.KnowledgeBaseItemService; +import jakarta.annotation.Resource; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/knowledge-base-item") +@Validated +public class KnowledgeBaseItemController { + + @Resource + private KnowledgeBaseItemService knowledgeBaseItemService; + + @PostMapping("/saveOrUpdate") + public KnowledgeBaseItem saveOrUpdate(@RequestBody KbItemEditReq itemEditReq) { + return knowledgeBaseItemService.saveOrUpdate(itemEditReq); + } + + @GetMapping("/search") + public Page search(String kbUuid, String keyword, @NotNull @Min(1) Integer currentPage, @NotNull @Min(10) Integer pageSize) { + return knowledgeBaseItemService.search(kbUuid, keyword, currentPage, pageSize); + } + + @GetMapping("/info/{uuid}") + public KnowledgeBaseItem info(@PathVariable String uuid) { + return knowledgeBaseItemService.lambdaQuery() + .eq(KnowledgeBaseItem::getUuid, uuid) + .eq(KnowledgeBaseItem::getIsDeleted, false) + .one(); + } + + @PostMapping("/embedding/{uuid}") + public boolean embedding(@PathVariable String uuid) { + return knowledgeBaseItemService.checkAndEmbedding(uuid); + } + + @PostMapping("/embedding-list") + public boolean embeddingBatch(@RequestBody KbItemEmbeddingBatchReq req) { + return knowledgeBaseItemService.checkAndEmbedding(req.getUuids()); + } + + @PostMapping("/del/{uuid}") + public boolean softDelete(@PathVariable String uuid) { + return knowledgeBaseItemService.softDelete(uuid); + } +} diff --git a/adi-chat/src/main/java/com/moyz/adi/chat/controller/KnowledgeBaseQAController.java b/adi-chat/src/main/java/com/moyz/adi/chat/controller/KnowledgeBaseQAController.java new file mode 100644 index 0000000..bf30ac5 --- /dev/null +++ b/adi-chat/src/main/java/com/moyz/adi/chat/controller/KnowledgeBaseQAController.java @@ -0,0 +1,40 @@ +package com.moyz.adi.chat.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.moyz.adi.common.dto.QAReq; +import com.moyz.adi.common.entity.KnowledgeBaseQaRecord; +import com.moyz.adi.common.service.KnowledgeBaseQaRecordService; +import com.moyz.adi.common.service.KnowledgeBaseService; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "知识库问答controller") +@RequestMapping("/knowledge-base/qa/") +@RestController +public class KnowledgeBaseQAController { + + @Resource + private KnowledgeBaseService knowledgeBaseService; + + @Resource + private KnowledgeBaseQaRecordService knowledgeBaseQaRecordService; + + @PostMapping("/ask/{kbUuid}") + public KnowledgeBaseQaRecord ask(@PathVariable String kbUuid, @RequestBody @Validated QAReq req) { + return knowledgeBaseService.answerAndRecord(kbUuid, req.getQuestion()); + } + + @GetMapping("/record/search") + public Page list(String kbUuid, String keyword, @NotNull @Min(1) Integer currentPage, @NotNull @Min(10) Integer pageSize) { + return knowledgeBaseQaRecordService.search(kbUuid, keyword, currentPage, pageSize); + } + + @PostMapping("/record/del/{uuid}") + public boolean recordDel(@PathVariable String uuid) { + return knowledgeBaseQaRecordService.softDelele(uuid); + } +} diff --git a/adi-chat/src/main/java/com/moyz/adi/chat/controller/UserController.java b/adi-chat/src/main/java/com/moyz/adi/chat/controller/UserController.java index 0a3ab06..0a10106 100644 --- a/adi-chat/src/main/java/com/moyz/adi/chat/controller/UserController.java +++ b/adi-chat/src/main/java/com/moyz/adi/chat/controller/UserController.java @@ -32,7 +32,7 @@ public class UserController { @Operation(summary = "用户信息") @GetMapping("/{uuid}") - public void login(@Validated @PathVariable String uuid) { + public void info(@Validated @PathVariable String uuid) { log.info(uuid); } diff --git a/adi-common/src/main/java/com/moyz/adi/common/CodeGenerator.java b/adi-common/src/main/java/com/moyz/adi/common/CodeGenerator.java index 8dccb39..37ac87e 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/CodeGenerator.java +++ b/adi-common/src/main/java/com/moyz/adi/common/CodeGenerator.java @@ -10,7 +10,7 @@ import java.util.Collections; public class CodeGenerator { public static void main(String[] args) { - FastAutoGenerator.create("jdbc:mysql://localhost:3306/aideepin?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&tinyInt1isBit=false&allowMultiQueries=true", "root", "123456") + FastAutoGenerator.create("jdbc:postgres://172.17.30.40:5432/aideepin?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&tinyInt1isBit=false&allowMultiQueries=true", "postgres", "postgres") .globalConfig(builder -> { builder.author("moyz") // 设置作者 .enableSwagger() // 开启 swagger 模式 @@ -35,7 +35,7 @@ public class CodeGenerator { .pathInfo(Collections.singletonMap(OutputFile.xml, "D://mybatisplus-generatorcode")); // 设置mapperXml生成路径 }) .strategyConfig(builder -> { - builder.addInclude("adi_user,adi_conversation,adi_conversation_message") // 设置需要生成的表名 + builder.addInclude("adi_knowledge_base_qa_record") // 设置需要生成的表名 .addTablePrefix("adi_"); builder.mapperBuilder().enableBaseResultMap().enableMapperAnnotation().build(); }) diff --git a/adi-common/src/main/java/com/moyz/adi/common/config/BeanConfig.java b/adi-common/src/main/java/com/moyz/adi/common/config/BeanConfig.java index 6ffcc99..6f2346c 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/config/BeanConfig.java +++ b/adi-common/src/main/java/com/moyz/adi/common/config/BeanConfig.java @@ -1,18 +1,18 @@ package com.moyz.adi.common.config; import com.baomidou.mybatisplus.annotation.DbType; -import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor; -import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; -import com.moyz.adi.common.util.LocalDateTimeUtil; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.google.common.collect.Lists; +import com.moyz.adi.common.util.LocalDateTimeUtil; +import com.pgvector.PGvector; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.session.SqlSessionFactory; import org.springframework.context.annotation.Bean; @@ -34,7 +34,7 @@ public class BeanConfig { @Bean public RestTemplate restTemplate() { - log.info("Configuration==create restTemplate"); + log.info("Configuration:create restTemplate"); SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); // 设置建立连接超时时间 毫秒 requestFactory.setConnectTimeout(60000); @@ -50,9 +50,9 @@ public class BeanConfig { @Bean @Primary - public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { - log.info("Configuration==create objectMapper"); - ObjectMapper objectMapper = builder.createXmlMapper(false).build(); + public ObjectMapper objectMapper() { + log.info("Configuration:create objectMapper"); + ObjectMapper objectMapper = new Jackson2ObjectMapperBuilder().createXmlMapper(false).build(); objectMapper.registerModules(LocalDateTimeUtil.getSimpleModule(), new JavaTimeModule(), new Jdk8Module()); //设置null值不参与序列化(字段不被显示) objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); @@ -88,12 +88,18 @@ public class BeanConfig { bean.setDataSource(dataSource); MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件 - interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL)); // 防止全表更新 interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); bean.setPlugins(interceptor); bean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/*.xml")); + MybatisConfiguration configuration = bean.getConfiguration(); + if(null == configuration){ + configuration = new MybatisConfiguration(); + bean.setConfiguration(configuration); + } + bean.getConfiguration().getTypeHandlerRegistry().register(PGvector.class, PostgresVectorTypeHandler.class); return bean.getObject(); } diff --git a/adi-common/src/main/java/com/moyz/adi/common/config/DevMockProperty.java b/adi-common/src/main/java/com/moyz/adi/common/config/DevMockProperty.java new file mode 100644 index 0000000..a41f643 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/config/DevMockProperty.java @@ -0,0 +1,11 @@ +package com.moyz.adi.common.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties("adi.dev-mock") +@Data +public class DevMockProperty { +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/config/PostgresVectorTypeHandler.java b/adi-common/src/main/java/com/moyz/adi/common/config/PostgresVectorTypeHandler.java new file mode 100644 index 0000000..f1f88f2 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/config/PostgresVectorTypeHandler.java @@ -0,0 +1,45 @@ +package com.moyz.adi.common.config; + +import com.pgvector.PGvector; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class PostgresVectorTypeHandler extends BaseTypeHandler { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, PGvector parameter, JdbcType jdbcType) + throws SQLException { + ps.setObject(i, parameter); +// ps.setArray(i, ps.getConnection().createArrayOf("float", parameter)); + } + + @Override + public PGvector getNullableResult(ResultSet rs, String columnName) throws SQLException { + return toFloatArray(rs.getArray(columnName)); + } + + @Override + public PGvector getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + return toFloatArray(rs.getArray(columnIndex)); + } + + @Override + public PGvector getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + return toFloatArray(cs.getArray(columnIndex)); + } + + private PGvector toFloatArray(java.sql.Array sqlArray) throws SQLException { + PGvector pGvector = new PGvector(new float[0]); + if (sqlArray == null) { + return pGvector; + } + pGvector.setValue(sqlArray.toString()); + return pGvector; + } + +} \ No newline at end of file diff --git a/adi-common/src/main/java/com/moyz/adi/common/cosntant/AdiConstant.java b/adi-common/src/main/java/com/moyz/adi/common/cosntant/AdiConstant.java index 20dbdc7..ba830d2 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/cosntant/AdiConstant.java +++ b/adi-common/src/main/java/com/moyz/adi/common/cosntant/AdiConstant.java @@ -54,8 +54,7 @@ public class AdiConstant { public static final List OPENAI_CREATE_IMAGE_SIZES = List.of("256x256", "512x512", "1024x1024"); - - public static class GenerateImage{ + public static class GenerateImage { public static final int INTERACTING_METHOD_GENERATE_IMAGE = 1; public static final int INTERACTING_METHOD_EDIT_IMAGE = 2; public static final int INTERACTING_METHOD_VARIATION = 3; @@ -78,4 +77,6 @@ public class AdiConstant { public static final String QUOTA_BY_IMAGE_MONTHLY = "quota_by_image_monthly"; } + + public static final String[] POI_DOC_TYPES = {"doc", "docx", "ppt", "pptx", "xls", "xlsx"}; } diff --git a/adi-common/src/main/java/com/moyz/adi/common/cosntant/RedisKeyConstant.java b/adi-common/src/main/java/com/moyz/adi/common/cosntant/RedisKeyConstant.java index 42ffc2f..6f0cbaa 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/cosntant/RedisKeyConstant.java +++ b/adi-common/src/main/java/com/moyz/adi/common/cosntant/RedisKeyConstant.java @@ -78,4 +78,17 @@ public class RedisKeyConstant { * 值: 用户id,用于校验后续流程中的重置密码使用 */ public static final String FIND_MY_PASSWORD = "user:find:password:{0}"; + + /** + * qa提问次数(每天) + * 参数:用户id:日期yyyyMMdd + * 值:提问数量 + */ + public static final String AQ_ASK_TIMES = "qa:ask:limit:{0}:{1}"; + + /** + * 知识库知识点生成数量 + * 值: 用户id + */ + public static final String qa_item_create_limit = "aq:item:create:{0}"; } diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/ConvMsgResp.java b/adi-common/src/main/java/com/moyz/adi/common/dto/ConvMsgResp.java index 729608f..b6afbd0 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/dto/ConvMsgResp.java +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/ConvMsgResp.java @@ -21,11 +21,11 @@ public class ConvMsgResp { private Long parentMessageId; @Schema(title = "对话的消息") - @TableField("content") - private String content; + @TableField("remark") + private String remark; @Schema(title = "产生该消息的角色:1: 用户,2:系统,3:助手") - private String messageRole; + private Integer messageRole; @Schema(title = "消耗的token数量") private Integer tokens; diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/KbEditReq.java b/adi-common/src/main/java/com/moyz/adi/common/dto/KbEditReq.java new file mode 100644 index 0000000..c4449e8 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/KbEditReq.java @@ -0,0 +1,21 @@ +package com.moyz.adi.common.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +@Data +@Validated +public class KbEditReq { + + private Long id; + + private String uuid; + + @NotBlank + private String title; + + private String remark; + + private Boolean isPublic; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/KbItemEditReq.java b/adi-common/src/main/java/com/moyz/adi/common/dto/KbItemEditReq.java new file mode 100644 index 0000000..ffb5623 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/KbItemEditReq.java @@ -0,0 +1,28 @@ +package com.moyz.adi.common.dto; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +@Data +@Validated +public class KbItemEditReq { + + private Long id; + + @Min(1) + private Long kbId; + + private String kbUuid; + + private String uuid; + + @NotBlank + private String title; + + private String brief; + + @NotBlank + private String remark; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/KbItemEmbeddingBatchReq.java b/adi-common/src/main/java/com/moyz/adi/common/dto/KbItemEmbeddingBatchReq.java new file mode 100644 index 0000000..f9d0e6d --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/KbItemEmbeddingBatchReq.java @@ -0,0 +1,8 @@ +package com.moyz.adi.common.dto; + +import lombok.Data; + +@Data +public class KbItemEmbeddingBatchReq { + private String[] uuids; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/KbItemEmbeddingDto.java b/adi-common/src/main/java/com/moyz/adi/common/dto/KbItemEmbeddingDto.java new file mode 100644 index 0000000..1333e9d --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/KbItemEmbeddingDto.java @@ -0,0 +1,12 @@ +package com.moyz.adi.common.dto; + +import lombok.Data; + +@Data +public class KbItemEmbeddingDto { + private String embeddingId; + + private float[] embedding; + + private String text; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/dto/QAReq.java b/adi-common/src/main/java/com/moyz/adi/common/dto/QAReq.java new file mode 100644 index 0000000..15e7bfb --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/dto/QAReq.java @@ -0,0 +1,13 @@ +package com.moyz.adi.common.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +@Validated +@Data +public class QAReq { + + @NotBlank + private String question; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/AdiFile.java b/adi-common/src/main/java/com/moyz/adi/common/entity/AdiFile.java index 1df831d..90827b4 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/entity/AdiFile.java +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/AdiFile.java @@ -37,7 +37,4 @@ public class AdiFile extends BaseEntity { @TableField(value = "ref_count") private Integer refCount; - @Schema(title = "是否删除(0:未删除,1:已删除)") - @TableField(value = "is_delete") - private Boolean isDelete; } diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/AiImage.java b/adi-common/src/main/java/com/moyz/adi/common/entity/AiImage.java index 3a30a5e..168fd3f 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/entity/AiImage.java +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/AiImage.java @@ -45,7 +45,4 @@ public class AiImage extends BaseEntity { @TableField("process_status") private Integer processStatus; - - @TableField("is_delete") - private Boolean isDelete; } diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/BaseEntity.java b/adi-common/src/main/java/com/moyz/adi/common/entity/BaseEntity.java index b96a84c..7bafd04 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/entity/BaseEntity.java +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/BaseEntity.java @@ -3,16 +3,13 @@ package com.moyz.adi.common.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; import java.io.Serializable; import java.time.LocalDateTime; -@Getter -@Setter -@ToString +@Data public class BaseEntity implements Serializable { private static final long serialVersionUID = 1L; @@ -26,4 +23,7 @@ public class BaseEntity implements Serializable { @TableField(value = "update_time") private LocalDateTime updateTime; + @Schema(title = "是否删除(0:未删除,1:已删除)") + @TableField(value = "is_deleted") + private Boolean isDeleted; } diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/Conversation.java b/adi-common/src/main/java/com/moyz/adi/common/entity/Conversation.java index d3e1384..0894864 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/entity/Conversation.java +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/Conversation.java @@ -45,7 +45,4 @@ public class Conversation extends BaseEntity { @Schema(title = "set the system message to ai, ig: you are a lawyer") @TableField("ai_system_message") private String aiSystemMessage; - - @TableField(value = "is_delete") - private Boolean isDelete; } diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/ConversationMessage.java b/adi-common/src/main/java/com/moyz/adi/common/entity/ConversationMessage.java index 1f1ee2d..6a460cb 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/entity/ConversationMessage.java +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/ConversationMessage.java @@ -39,12 +39,12 @@ public class ConversationMessage extends BaseEntity { private Long userId; @Schema(title = "对话的消息") - @TableField("content") - private String content; + @TableField("remark") + private String remark; @Schema(title = "产生该消息的角色:1: 用户,2:系统,3:助手") @TableField("message_role") - private String messageRole; + private Integer messageRole; @Schema(title = "消耗的token数量") @TableField("tokens") @@ -57,7 +57,4 @@ public class ConversationMessage extends BaseEntity { @Schema(name = "上下文理解中携带的消息对数量(提示词及回复)") @TableField("understand_context_msg_pair_num") private Integer understandContextMsgPairNum; - - @TableField(value = "is_delete") - private Boolean isDelete; } diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/KnowledgeBase.java b/adi-common/src/main/java/com/moyz/adi/common/entity/KnowledgeBase.java new file mode 100644 index 0000000..da40fe7 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/KnowledgeBase.java @@ -0,0 +1,36 @@ +package com.moyz.adi.common.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@TableName("adi_knowledge_base") +@Schema(title = "知识库实体", description = "知识库表") +public class KnowledgeBase extends BaseEntity { + + @Schema(title = "uuid") + @TableField("uuid") + private String uuid; + + @Schema(title = "名称") + @TableField("title") + private String title; + + @Schema(title = "描述") + @TableField("remark") + private String remark; + + @Schema(title = "是否公开") + @TableField("is_public") + private Boolean isPublic; + + @Schema(title = "所属人id") + @TableField("owner_id") + private Long ownerId; + + @Schema(title = "所属人名称") + @TableField("owner_name") + private String ownerName; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/KnowledgeBaseEmbedding.java b/adi-common/src/main/java/com/moyz/adi/common/entity/KnowledgeBaseEmbedding.java new file mode 100644 index 0000000..59cc4c2 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/KnowledgeBaseEmbedding.java @@ -0,0 +1,25 @@ +package com.moyz.adi.common.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.pgvector.PGvector; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@TableName("adi_knowledge_base_embedding") +@Schema(title = "知识库-嵌入实体", description = "知识库嵌入表") +public class KnowledgeBaseEmbedding extends BaseEntity { + + @Schema(title = "embedding uuid") + @TableField("embedding") + private String embeddingId; + + @Schema(title = "embedding") + @TableField("embedding") + private PGvector embedding; + + @Schema(title = "对应的文档") + @TableField("text") + private String text; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/KnowledgeBaseItem.java b/adi-common/src/main/java/com/moyz/adi/common/entity/KnowledgeBaseItem.java new file mode 100644 index 0000000..5af6ad2 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/KnowledgeBaseItem.java @@ -0,0 +1,44 @@ +package com.moyz.adi.common.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@TableName("adi_knowledge_base_item") +@Schema(title = "知识库条目实体", description = "知识库条目表") +public class KnowledgeBaseItem extends BaseEntity { + + @Schema(title = "知识库id") + @TableField("kb_id") + private Long kbId; + + @Schema(title = "知识库uuid") + @TableField("kb_uuid") + private String kbUuid; + + @Schema(title = "名称") + @TableField("source_file_id") + private Long sourceFileId; + + @Schema(title = "uuid") + @TableField("uuid") + private String uuid; + + @Schema(title = "标题") + @TableField("title") + private String title; + + @Schema(title = "内容摘要") + @TableField("brief") + private String brief; + + @Schema(title = "内容") + @TableField("remark") + private String remark; + + @Schema(title = "是否已向量化") + @TableField("is_embedded") + private Boolean isEmbedded; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/KnowledgeBaseQaRecord.java b/adi-common/src/main/java/com/moyz/adi/common/entity/KnowledgeBaseQaRecord.java new file mode 100644 index 0000000..3c32c83 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/KnowledgeBaseQaRecord.java @@ -0,0 +1,41 @@ +package com.moyz.adi.common.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.apache.ibatis.type.JdbcType; + +@Data +@TableName("adi_knowledge_base_qa_record") +@Schema(title = "知识库问答记录实体", description = "知识库问答记录表") +public class KnowledgeBaseQaRecord extends BaseEntity { + + @Schema(title = "uuid") + @TableField(value = "uuid", jdbcType = JdbcType.VARCHAR) + private String uuid; + + @Schema(title = "知识库id") + @TableField("kb_id") + private Long kbId; + + @Schema(title = "知识库uuid") + @TableField("kb_uuid") + private String kbUuid; + + @Schema(title = "来源文档id,以逗号隔开") + @TableField("source_file_ids") + private String sourceFileIds; + + @Schema(title = "问题") + @TableField("question") + private String question; + + @Schema(title = "答案") + @TableField("answer") + private String answer; + + @Schema(title = "提问用户id") + @TableField("user_id") + private Long userId; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/Prompt.java b/adi-common/src/main/java/com/moyz/adi/common/entity/Prompt.java index d384ea0..5262c10 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/entity/Prompt.java +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/Prompt.java @@ -22,7 +22,4 @@ public class Prompt extends BaseEntity { @TableField(value = "prompt") private String prompt; - @Schema(title = "是否删除(0:未删除,1:已删除)") - @TableField(value = "is_delete") - private Boolean isDelete; } diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/SysConfig.java b/adi-common/src/main/java/com/moyz/adi/common/entity/SysConfig.java index 9c93779..55c0858 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/entity/SysConfig.java +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/SysConfig.java @@ -17,7 +17,4 @@ public class SysConfig extends BaseEntity { @Schema(title = "配置项的值") private String value; - @Schema(title = "是否删除(0:未删除,1:已删除)") - @TableField(value = "is_delete") - private Boolean isDelete; } diff --git a/adi-common/src/main/java/com/moyz/adi/common/entity/User.java b/adi-common/src/main/java/com/moyz/adi/common/entity/User.java index 4631932..08108d6 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/entity/User.java +++ b/adi-common/src/main/java/com/moyz/adi/common/entity/User.java @@ -62,6 +62,7 @@ public class User extends BaseEntity { @TableField("active_time") private LocalDateTime activeTime; - @TableField("is_delete") - private Boolean isDelete; + @Schema(title = "是否管理员(0:否,1:是)") + @TableField(value = "is_admin") + private Boolean isAdmin; } diff --git a/adi-common/src/main/java/com/moyz/adi/common/enums/ChatMessageRoleEnum.java b/adi-common/src/main/java/com/moyz/adi/common/enums/ChatMessageRoleEnum.java new file mode 100644 index 0000000..e8c365c --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/enums/ChatMessageRoleEnum.java @@ -0,0 +1,18 @@ +package com.moyz.adi.common.enums; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ChatMessageRoleEnum implements BaseEnum { + + USER(1, "user"), + SYSTEM(2, "system"), + + ASSISTANT(3, "assistant"); + + private final Integer value; + private final String desc; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/enums/ErrorEnum.java b/adi-common/src/main/java/com/moyz/adi/common/enums/ErrorEnum.java index af190e3..4a9d1e7 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/enums/ErrorEnum.java +++ b/adi-common/src/main/java/com/moyz/adi/common/enums/ErrorEnum.java @@ -17,6 +17,11 @@ public enum ErrorEnum { A_REGISTER_USER_EXIST("A0013", "账号已经存在,请使用账号密码登录"), A_FIND_PASSWORD_CODE_ERROR("A0014", "重置码已过期或不存在"), A_USER_WAIT_CONFIRM("A0015", "用户未激活"), + A_USER_NOT_AUTH("A0016", "用户无权限"), + A_DATA_NOT_FOUND("A0017", "数据不存在"), + A_UPLOAD_FAIL("A0018", "上传失败"), + A_QA_ASK_LIMIT("A0019", "请求次数太多"), + A_QA_ITEM_LIMIT("A0020", "知识点生成已超额度"), B_UNCAUGHT_ERROR("B0001", "未捕捉异常"), B_COMMON_ERROR("B0002", "业务出错"), B_GLOBAL_ERROR("B0003", "全局异常"), diff --git a/adi-common/src/main/java/com/moyz/adi/common/filter/TokenFilter.java b/adi-common/src/main/java/com/moyz/adi/common/filter/TokenFilter.java index 7d78be9..7bc59ff 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/filter/TokenFilter.java +++ b/adi-common/src/main/java/com/moyz/adi/common/filter/TokenFilter.java @@ -29,7 +29,7 @@ import static org.springframework.http.HttpHeaders.AUTHORIZATION; public class TokenFilter extends OncePerRequestFilter { public static final String[] EXCLUDE_API = { - "/auth/", + "/auth/" }; @Resource diff --git a/adi-common/src/main/java/com/moyz/adi/common/helper/EmbeddingHelper.java b/adi-common/src/main/java/com/moyz/adi/common/helper/EmbeddingHelper.java new file mode 100644 index 0000000..df8e273 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/helper/EmbeddingHelper.java @@ -0,0 +1,156 @@ +package com.moyz.adi.common.helper; + +import com.moyz.adi.common.util.AdiPgVectorEmbeddingStore; +import dev.langchain4j.data.document.DocumentSplitter; +import dev.langchain4j.data.document.splitter.DocumentSplitters; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.model.embedding.AllMiniLmL6V2EmbeddingModel; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.input.Prompt; +import dev.langchain4j.model.input.PromptTemplate; +import dev.langchain4j.model.openai.OpenAiChatModel; +import dev.langchain4j.model.openai.OpenAiTokenizer; +import dev.langchain4j.store.embedding.EmbeddingMatch; +import dev.langchain4j.store.embedding.EmbeddingStore; +import dev.langchain4j.store.embedding.EmbeddingStoreIngestor; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static dev.langchain4j.model.openai.OpenAiModelName.GPT_3_5_TURBO; +import static java.util.stream.Collectors.joining; + +@Slf4j +@Service +public class EmbeddingHelper { + + @Value("${spring.datasource.url}") + private String dataBaseUrl; + + @Value("${spring.datasource.username}") + private String dataBaseUserName; + + @Value("${spring.datasource.password}") + private String dataBasePassword; + + @Value("${openai.proxy.enable:false}") + private boolean proxyEnable; + + @Value("${openai.proxy.host:0}") + private String proxyHost; + + @Value("${openai.proxy.http-port:0}") + private int proxyHttpPort; + + private static final PromptTemplate promptTemplate = PromptTemplate.from("尽可能准确地回答下面的问题: {{question}}\n\n根据以下知识库的内容:\n{{information}}"); + + @Resource + private OpenAiHelper openAiHelper; + + private EmbeddingModel embeddingModel; + + private EmbeddingStore embeddingStore; + + private ChatLanguageModel chatLanguageModel; + + public void init() { + log.info("initEmbeddingModel"); + embeddingModel = new AllMiniLmL6V2EmbeddingModel(); + embeddingStore = initEmbeddingStore(); + chatLanguageModel = initChatLanguageModel(); + } + + private EmbeddingStore initEmbeddingStore() { + // 正则表达式匹配 + String regex = "jdbc:postgresql://([^:/]+):(\\d+)/(\\w+).+"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(dataBaseUrl); + + String host = ""; + String port = ""; + String databaseName = ""; + if (matcher.matches()) { + host = matcher.group(1); + port = matcher.group(2); + databaseName = matcher.group(3); + + System.out.println("Host: " + host); + System.out.println("Port: " + port); + System.out.println("Database: " + databaseName); + } else { + throw new RuntimeException("parse url error"); + } + AdiPgVectorEmbeddingStore embeddingStore = AdiPgVectorEmbeddingStore.builder() + .host(host) + .port(Integer.parseInt(port)) + .database(databaseName) + .user(dataBaseUserName) + .password(dataBasePassword) + .dimension(384) + .createTable(true) + .dropTableFirst(false) + .table("adi_knowledge_base_embedding") + .build(); + return embeddingStore; + } + + private ChatLanguageModel initChatLanguageModel() { + OpenAiChatModel.OpenAiChatModelBuilder builder = OpenAiChatModel.builder().apiKey(openAiHelper.getSecretKey()); + if (proxyEnable) { + Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyHttpPort)); + builder.proxy(proxy); + } + return builder.build(); + } + + public EmbeddingStoreIngestor getEmbeddingStoreIngestor() { + DocumentSplitter documentSplitter = DocumentSplitters.recursive(1000, 0, new OpenAiTokenizer(GPT_3_5_TURBO)); + EmbeddingStoreIngestor embeddingStoreIngestor = EmbeddingStoreIngestor.builder() + .documentSplitter(documentSplitter) + .embeddingModel(embeddingModel) + .embeddingStore(embeddingStore) + .build(); + return embeddingStoreIngestor; + } + + public String findAnswer(String kbUuid, String question) { + + // Embed the question + Embedding questionEmbedding = embeddingModel.embed(question).content(); + + // Find relevant embeddings in embedding store by semantic similarity + // You can play with parameters below to find a sweet spot for your specific use case + int maxResults = 3; + double minScore = 0.6; + List> relevantEmbeddings = ((AdiPgVectorEmbeddingStore) embeddingStore).findRelevantByKbUuid(kbUuid, questionEmbedding, maxResults, minScore); + + // Create a prompt for the model that includes question and relevant embeddings + String information = relevantEmbeddings.stream() + .map(match -> match.embedded().text()) + .collect(joining("\n\n")); + + if (StringUtils.isBlank(information)) { + return StringUtils.EMPTY; + } + Prompt prompt = promptTemplate.apply(Map.of("question", question, "information", Matcher.quoteReplacement(information))); + + AiMessage aiMessage = chatLanguageModel.generate(prompt.toUserMessage()).content(); + + // See an answer from the model + return aiMessage.text(); + } + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/helper/OpenAiHelper.java b/adi-common/src/main/java/com/moyz/adi/common/helper/OpenAiHelper.java index bd7d402..f8bcbe8 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/helper/OpenAiHelper.java +++ b/adi-common/src/main/java/com/moyz/adi/common/helper/OpenAiHelper.java @@ -1,30 +1,34 @@ package com.moyz.adi.common.helper; -import com.didalgo.gpt3.ChatFormatDescriptor; -import com.didalgo.gpt3.Encoding; -import com.didalgo.gpt3.GPT3Tokenizer; -import com.didalgo.gpt3.TokenCount; import com.fasterxml.jackson.databind.ObjectMapper; -import com.moyz.adi.common.cosntant.AdiConstant; +import com.moyz.adi.common.base.ThreadContext; import com.moyz.adi.common.entity.AiImage; import com.moyz.adi.common.entity.User; import com.moyz.adi.common.enums.ErrorEnum; import com.moyz.adi.common.exception.BaseException; -import com.moyz.adi.common.model.AnswerMeta; -import com.moyz.adi.common.model.ChatMeta; -import com.moyz.adi.common.model.QuestionMeta; +import com.moyz.adi.common.interfaces.IChatAssistant; import com.moyz.adi.common.service.FileService; import com.moyz.adi.common.service.SysConfigService; -import com.moyz.adi.common.util.FileUtil; import com.moyz.adi.common.util.ImageUtil; import com.moyz.adi.common.util.JsonUtil; import com.moyz.adi.common.util.TriConsumer; +import com.moyz.adi.common.vo.AnswerMeta; +import com.moyz.adi.common.vo.ChatMeta; +import com.moyz.adi.common.vo.QuestionMeta; +import com.moyz.adi.common.vo.SseAskParams; import com.theokanning.openai.OpenAiApi; -import com.theokanning.openai.completion.chat.ChatCompletionChoice; -import com.theokanning.openai.completion.chat.ChatCompletionRequest; -import com.theokanning.openai.completion.chat.ChatMessage; -import com.theokanning.openai.image.*; +import com.theokanning.openai.image.CreateImageEditRequest; +import com.theokanning.openai.image.CreateImageVariationRequest; +import com.theokanning.openai.image.ImageResult; import com.theokanning.openai.service.OpenAiService; +import dev.langchain4j.data.image.Image; +import dev.langchain4j.memory.ChatMemory; +import dev.langchain4j.model.image.ImageModel; +import dev.langchain4j.model.openai.OpenAiImageModel; +import dev.langchain4j.model.openai.OpenAiStreamingChatModel; +import dev.langchain4j.model.output.Response; +import dev.langchain4j.service.AiServices; +import dev.langchain4j.service.TokenStream; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import okhttp3.OkHttpClient; @@ -35,19 +39,23 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import retrofit2.Retrofit; import java.io.File; +import java.io.IOException; import java.net.InetSocketAddress; import java.net.Proxy; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; import static com.moyz.adi.common.cosntant.AdiConstant.OPENAI_CREATE_IMAGE_RESP_FORMATS_URL; import static com.moyz.adi.common.cosntant.AdiConstant.OPENAI_CREATE_IMAGE_SIZES; import static com.theokanning.openai.service.OpenAiService.defaultClient; import static com.theokanning.openai.service.OpenAiService.defaultRetrofit; +import static dev.ai4j.openai4j.image.ImageModel.DALL_E_SIZE_1024_x_1024; +import static dev.ai4j.openai4j.image.ImageModel.DALL_E_SIZE_512_x_512; +import static dev.langchain4j.model.openai.OpenAiModelName.DALL_E_2; @Slf4j @Service @@ -62,21 +70,23 @@ public class OpenAiHelper { @Value("${openai.proxy.http-port:0}") private int proxyHttpPort; - @Value("${local.images}") - private String localImagesPath; - @Resource private FileService fileService; @Resource private ObjectMapper objectMapper; - public OpenAiService getOpenAiService(User user) { + public String getSecretKey() { String secretKey = SysConfigService.getSecretKey(); - String userSecretKey = user.getSecretKey(); - if (StringUtils.isNotBlank(userSecretKey)) { - secretKey = userSecretKey; + User user = ThreadContext.getCurrentUser(); + if (null != user && StringUtils.isNotBlank(user.getSecretKey())) { + secretKey = user.getSecretKey(); } + return secretKey; + } + + public OpenAiService getOpenAiService() { + String secretKey = getSecretKey(); if (proxyEnable) { Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyHttpPort)); OkHttpClient client = defaultClient(secretKey, Duration.of(60, ChronoUnit.SECONDS)) @@ -90,102 +100,121 @@ public class OpenAiHelper { return new OpenAiService(secretKey, Duration.of(60, ChronoUnit.SECONDS)); } - /** - * Send http request to openai server
- * Calculate token - * - * @param user - * @param regenerateQuestionUuid - * @param chatMessageList - * @param sseEmitter - * @param consumer - */ - public void sseAsk(User user, String regenerateQuestionUuid, List chatMessageList, SseEmitter sseEmitter, TriConsumer consumer) { - final int[] answerTokens = {0}; - StringBuilder response = new StringBuilder(); - OpenAiService service = getOpenAiService(user); - ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest - .builder() - .model(AdiConstant.DEFAULT_MODEL) - .messages(chatMessageList) - .n(1) - .logitBias(new HashMap<>()) - .build(); - service.streamChatCompletion(chatCompletionRequest) - .doOnError(onError -> { - log.error("openai error", onError); - sseEmitter.send(SseEmitter.event().name("error").data(onError.getMessage())); - sseEmitter.complete(); - }).subscribe(completionChunk -> { - answerTokens[0]++; - List choices = completionChunk.getChoices(); - String content = choices.get(0).getMessage().getContent(); - log.info("get content:{}", content); - if (null == content && response.isEmpty()) { - return; - } - if (null == content || AdiConstant.OPENAI_MESSAGE_DONE_FLAG.equals(content)) { - log.info("OpenAI返回数据结束了"); - sseEmitter.send(AdiConstant.OPENAI_MESSAGE_DONE_FLAG); - - - GPT3Tokenizer tokenizer = new GPT3Tokenizer(Encoding.CL100K_BASE); - int questionTokens = 0; - try { - questionTokens = TokenCount.fromMessages(chatMessageList, tokenizer, ChatFormatDescriptor.forModel(AdiConstant.DEFAULT_MODEL)); - } catch (IllegalArgumentException e) { - log.error("该模型的token无法统计,model:{}", AdiConstant.DEFAULT_MODEL); - } - System.out.println("requestTokens:" + questionTokens); - System.out.println("返回内容:" + response); - - String questionUuid = StringUtils.isNotBlank(regenerateQuestionUuid) ? regenerateQuestionUuid : UUID.randomUUID().toString().replace("-", ""); - QuestionMeta questionMeta = new QuestionMeta(questionTokens, questionUuid); - AnswerMeta answerMeta = new AnswerMeta(answerTokens[0], UUID.randomUUID().toString().replace("-", "")); - ChatMeta chatMeta = new ChatMeta(questionMeta, answerMeta); -// String meta = JsonUtil.toJson(chatMeta).replaceAll("\r\n", ""); - String meta = JsonUtil.toJson(chatMeta).replaceAll("\r\n", ""); - log.info("meta:" + meta); - sseEmitter.send(" [META]" + meta); - // close eventSourceEmitter after tokens was calculated - sseEmitter.complete(); - consumer.accept(response.toString(), questionMeta, answerMeta); - return; - } - //加空格配合前端的fetchEventSource进行解析,见https://github.com/Azure/fetch-event-source/blob/45ac3cfffd30b05b79fbf95c21e67d4ef59aa56a/src/parse.ts#L129-L133 - sseEmitter.send(" " + content); - response.append(content); - }); - - - System.out.println("返回内容1111:" + response); + public IChatAssistant getChatAssistant(ChatMemory chatMemory) { + String secretKey = getSecretKey(); + OpenAiStreamingChatModel.OpenAiStreamingChatModelBuilder builder = OpenAiStreamingChatModel.builder(); + if (proxyEnable) { + Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyHttpPort)); + builder.proxy(proxy); + } + builder.apiKey(secretKey).timeout(Duration.of(60, ChronoUnit.SECONDS)); + AiServices serviceBuilder = AiServices.builder(IChatAssistant.class) + .streamingChatLanguageModel(builder.build()); + if (null != chatMemory) { + serviceBuilder.chatMemory(chatMemory); + } + return serviceBuilder.build(); } - public List createImage(User user, AiImage aiImage) { + public ImageModel getImageModel(User user, String size) { + String secretKey = getSecretKey(); + if (proxyEnable) { + Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyHttpPort)); + return OpenAiImageModel.builder() + .modelName(DALL_E_2) + .apiKey(secretKey) + .user(user.getUuid()) + .responseFormat(OPENAI_CREATE_IMAGE_RESP_FORMATS_URL) + .size(StringUtils.defaultString(size, DALL_E_SIZE_512_x_512)) + .logRequests(true) + .logResponses(true) + .withPersisting(false) + .maxRetries(2) + .proxy(proxy) + .build(); + } + return OpenAiImageModel.builder() + .modelName(DALL_E_2) + .apiKey(secretKey) + .user(user.getUuid()) + .responseFormat(OPENAI_CREATE_IMAGE_RESP_FORMATS_URL) + .size(StringUtils.defaultString(size, DALL_E_SIZE_512_x_512)) + .logRequests(true) + .logResponses(true) + .withPersisting(false) + .maxRetries(2) + .build(); + } + + /** + * Send http request to llm server + */ + public void sseAsk(SseAskParams params, TriConsumer consumer) { + IChatAssistant chatAssistant = getChatAssistant(params.getChatMemory()); + TokenStream tokenStream; + if (StringUtils.isNotBlank(params.getSystemMessage())) { + tokenStream = chatAssistant.chat(params.getSystemMessage(), params.getUserMessage()); + } else { + tokenStream = chatAssistant.chat(params.getUserMessage()); + } + tokenStream.onNext((content) -> { + log.info("get content:{}", content); + //加空格配合前端的fetchEventSource进行解析,见https://github.com/Azure/fetch-event-source/blob/45ac3cfffd30b05b79fbf95c21e67d4ef59aa56a/src/parse.ts#L129-L133 + try { + params.getSseEmitter().send(" " + content); + } catch (IOException e) { + log.error("stream onNext error", e); + } + }) + .onComplete((response) -> { + log.info("返回数据结束了:{}", response); + String questionUuid = StringUtils.isNotBlank(params.getRegenerateQuestionUuid()) ? params.getRegenerateQuestionUuid() : UUID.randomUUID().toString().replace("-", ""); + QuestionMeta questionMeta = new QuestionMeta(response.tokenUsage().inputTokenCount(), questionUuid); + AnswerMeta answerMeta = new AnswerMeta(response.tokenUsage().outputTokenCount(), UUID.randomUUID().toString().replace("-", "")); + ChatMeta chatMeta = new ChatMeta(questionMeta, answerMeta); + String meta = JsonUtil.toJson(chatMeta).replaceAll("\r\n", ""); + log.info("meta:" + meta); + try { + params.getSseEmitter().send(" [META]" + meta); + } catch (IOException e) { + log.error("stream onComplete error", e); + throw new RuntimeException(e); + } + // close eventSourceEmitter after tokens was calculated + params.getSseEmitter().complete(); + consumer.accept(response.content().text(), questionMeta, answerMeta); + }) + .onError((error) -> { + log.error("stream error", error); + try { + params.getSseEmitter().send(SseEmitter.event().name("error").data(error.getMessage())); + } catch (IOException e) { + log.error("sse error", e); + } + params.getSseEmitter().complete(); + }) + .start(); + } + + public List createImage(User user, AiImage aiImage) { if (aiImage.getGenerateNumber() < 1 || aiImage.getGenerateNumber() > 10) { throw new BaseException(ErrorEnum.A_IMAGE_NUMBER_ERROR); } if (!OPENAI_CREATE_IMAGE_SIZES.contains(aiImage.getGenerateSize())) { throw new BaseException(ErrorEnum.A_IMAGE_SIZE_ERROR); } - OpenAiService service = getOpenAiService(user); - CreateImageRequest createImageRequest = new CreateImageRequest(); - createImageRequest.setPrompt(aiImage.getPrompt()); - createImageRequest.setN(aiImage.getGenerateNumber()); - createImageRequest.setSize(aiImage.getGenerateSize()); - createImageRequest.setResponseFormat(OPENAI_CREATE_IMAGE_RESP_FORMATS_URL); - createImageRequest.setUser(user.getUuid()); + ImageModel imageModel = getImageModel(user, aiImage.getGenerateSize()); try { - ImageResult imageResult = service.createImage(createImageRequest); - log.info("createImage response:{}", imageResult); - return imageResult.getData(); + Response> response = imageModel.generate(aiImage.getPrompt(), aiImage.getGenerateNumber()); + log.info("createImage response:{}", response); + return response.content().stream().map(item -> item.url().toString()).collect(Collectors.toList()); } catch (Exception e) { log.error("create image error", e); } return Collections.emptyList(); } - public List editImage(User user, AiImage aiImage) { + public List editImage(User user, AiImage aiImage) { File originalFile = new File(fileService.getImagePath(aiImage.getOriginalImage())); File maskFile = null; if (StringUtils.isNotBlank(aiImage.getMaskImage())) { @@ -193,7 +222,7 @@ public class OpenAiHelper { } //如果不是RGBA类型的图片,先转成RGBA File rgbaOriginalImage = ImageUtil.rgbConvertToRgba(originalFile, fileService.getTmpImagesPath(aiImage.getOriginalImage())); - OpenAiService service = getOpenAiService(user); + OpenAiService service = getOpenAiService(); CreateImageEditRequest request = new CreateImageEditRequest(); request.setPrompt(aiImage.getPrompt()); request.setN(aiImage.getGenerateNumber()); @@ -203,16 +232,16 @@ public class OpenAiHelper { try { ImageResult imageResult = service.createImageEdit(request, rgbaOriginalImage, maskFile); log.info("editImage response:{}", imageResult); - return imageResult.getData(); + return imageResult.getData().stream().map(item -> item.getUrl()).collect(Collectors.toList()); } catch (Exception e) { log.error("edit image error", e); } return Collections.emptyList(); } - public List createImageVariation(User user, AiImage aiImage) { + public List createImageVariation(User user, AiImage aiImage) { File imagePath = new File(fileService.getImagePath(aiImage.getOriginalImage())); - OpenAiService service = getOpenAiService(user); + OpenAiService service = getOpenAiService(); CreateImageVariationRequest request = new CreateImageVariationRequest(); request.setN(aiImage.getGenerateNumber()); request.setSize(aiImage.getGenerateSize()); @@ -221,7 +250,7 @@ public class OpenAiHelper { try { ImageResult imageResult = service.createImageVariation(request, imagePath); log.info("createImageVariation response:{}", imageResult); - return imageResult.getData(); + return imageResult.getData().stream().map(item -> item.getUrl()).collect(Collectors.toList()); } catch (Exception e) { log.error("image variation error", e); } diff --git a/adi-common/src/main/java/com/moyz/adi/common/helper/QuotaHelper.java b/adi-common/src/main/java/com/moyz/adi/common/helper/QuotaHelper.java index 6be34f7..ed82c6f 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/helper/QuotaHelper.java +++ b/adi-common/src/main/java/com/moyz/adi/common/helper/QuotaHelper.java @@ -2,7 +2,7 @@ package com.moyz.adi.common.helper; import com.moyz.adi.common.entity.User; import com.moyz.adi.common.enums.ErrorEnum; -import com.moyz.adi.common.model.CostStat; +import com.moyz.adi.common.vo.CostStat; import com.moyz.adi.common.service.UserDayCostService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; diff --git a/adi-common/src/main/java/com/moyz/adi/common/helper/RateLimitHelper.java b/adi-common/src/main/java/com/moyz/adi/common/helper/RateLimitHelper.java index 1e8734a..a6b527e 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/helper/RateLimitHelper.java +++ b/adi-common/src/main/java/com/moyz/adi/common/helper/RateLimitHelper.java @@ -1,6 +1,6 @@ package com.moyz.adi.common.helper; -import com.moyz.adi.common.model.RequestRateLimit; +import com.moyz.adi.common.vo.RequestRateLimit; import jakarta.annotation.Resource; import org.apache.commons.lang3.StringUtils; import org.springframework.data.redis.core.StringRedisTemplate; diff --git a/adi-common/src/main/java/com/moyz/adi/common/interfaces/IChatAssistant.java b/adi-common/src/main/java/com/moyz/adi/common/interfaces/IChatAssistant.java new file mode 100644 index 0000000..77f8457 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/interfaces/IChatAssistant.java @@ -0,0 +1,14 @@ +package com.moyz.adi.common.interfaces; + +import dev.langchain4j.service.SystemMessage; +import dev.langchain4j.service.TokenStream; +import dev.langchain4j.service.UserMessage; +import dev.langchain4j.service.V; + +public interface IChatAssistant { + + @SystemMessage("{{sm}}") + TokenStream chat(@V("sm") String systemMessage, @UserMessage String prompt); + + TokenStream chat(@UserMessage String prompt); +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/mapper/KnowledgeBaseEmbeddingMapper.java b/adi-common/src/main/java/com/moyz/adi/common/mapper/KnowledgeBaseEmbeddingMapper.java new file mode 100644 index 0000000..dbd8da3 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/mapper/KnowledgeBaseEmbeddingMapper.java @@ -0,0 +1,17 @@ +package com.moyz.adi.common.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.moyz.adi.common.entity.KnowledgeBaseEmbedding; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +@Mapper +public interface KnowledgeBaseEmbeddingMapper extends BaseMapper { + + Page selectByItemUuid(Page page, @Param("kbItemUuid") String uuid); + + boolean deleteByItemUuid(@Param("kbItemUuid") String uuid); +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/mapper/KnowledgeBaseItemMapper.java b/adi-common/src/main/java/com/moyz/adi/common/mapper/KnowledgeBaseItemMapper.java new file mode 100644 index 0000000..2fe223e --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/mapper/KnowledgeBaseItemMapper.java @@ -0,0 +1,9 @@ +package com.moyz.adi.common.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.moyz.adi.common.entity.KnowledgeBaseItem; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface KnowledgeBaseItemMapper extends BaseMapper { +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/mapper/KnowledgeBaseMapper.java b/adi-common/src/main/java/com/moyz/adi/common/mapper/KnowledgeBaseMapper.java new file mode 100644 index 0000000..d01b77f --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/mapper/KnowledgeBaseMapper.java @@ -0,0 +1,28 @@ +package com.moyz.adi.common.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.moyz.adi.common.entity.KnowledgeBase; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface KnowledgeBaseMapper extends BaseMapper { + + /** + * 搜索知识库(管理员) + * + * @param keyword 关键词 + * @return + */ + Page searchByAdmin(Page page, @Param("keyword") String keyword); + + /** + * 搜索知识库(用户) + * + * @param ownerId 用户id + * @param keyword 关键词 + * @return + */ + Page searchByUser(Page page, @Param("ownerId") long ownerId, @Param("keyword") String keyword, @Param("includeOthersPublic") Boolean includeOthersPublic); +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/mapper/KnowledgeBaseQaRecordMapper.java b/adi-common/src/main/java/com/moyz/adi/common/mapper/KnowledgeBaseQaRecordMapper.java new file mode 100644 index 0000000..30558d3 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/mapper/KnowledgeBaseQaRecordMapper.java @@ -0,0 +1,10 @@ +package com.moyz.adi.common.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.moyz.adi.common.entity.KnowledgeBaseQaRecord; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface KnowledgeBaseQaRecordMapper extends BaseMapper { + +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/AiImageService.java b/adi-common/src/main/java/com/moyz/adi/common/service/AiImageService.java index 49f2bae..1a20eaa 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/service/AiImageService.java +++ b/adi-common/src/main/java/com/moyz/adi/common/service/AiImageService.java @@ -17,7 +17,6 @@ import com.moyz.adi.common.mapper.AiImageMapper; import com.moyz.adi.common.util.LocalCache; import com.moyz.adi.common.util.LocalDateTimeUtil; import com.moyz.adi.common.util.UserUtil; -import com.theokanning.openai.image.Image; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -174,7 +173,7 @@ public class AiImageService extends ServiceImpl { String requestTimesKey = MessageFormat.format(RedisKeyConstant.USER_REQUEST_TEXT_TIMES, user.getId()); rateLimitHelper.increaseRequestTimes(requestTimesKey, LocalCache.IMAGE_RATE_LIMIT_CONFIG); - List images = new ArrayList<>(); + List images = new ArrayList<>(); if (aiImage.getInteractingMethod() == INTERACTING_METHOD_GENERATE_IMAGE) { images = openAiHelper.createImage(user, aiImage); } else if (aiImage.getInteractingMethod() == INTERACTING_METHOD_EDIT_IMAGE) { @@ -183,8 +182,8 @@ public class AiImageService extends ServiceImpl { images = openAiHelper.createImageVariation(user, aiImage); } List imageUuids = new ArrayList(); - images.forEach(image -> { - String imageUuid = fileService.saveToLocal(user, image.getUrl()); + images.forEach(imageUrl -> { + String imageUuid = fileService.saveToLocal(user, imageUrl); imageUuids.add(imageUuid); }); String imageUuidsJoin = imageUuids.stream().collect(Collectors.joining(",")); @@ -192,7 +191,7 @@ public class AiImageService extends ServiceImpl { _this.lambdaUpdate().eq(AiImage::getId, aiImage.getId()).set(AiImage::getProcessStatus, STATUS_FAIL).update(); return; } - String respImagesPath = images.stream().map(Image::getUrl).collect(Collectors.joining(",")); + String respImagesPath = images.stream().collect(Collectors.joining(",")); updateAiImageStatus(aiImage.getId(), respImagesPath, imageUuidsJoin, STATUS_SUCCESS); //Update the cost of current user @@ -231,7 +230,7 @@ public class AiImageService extends ServiceImpl { public AiImagesListResp listAll(@RequestParam Long maxId, @RequestParam int pageSize) { List list = this.lambdaQuery() .eq(AiImage::getUserId, ThreadContext.getCurrentUserId()) - .eq(AiImage::getIsDelete, false) + .eq(AiImage::getIsDeleted, false) .lt(AiImage::getId, maxId) .orderByDesc(AiImage::getId) .last("limit " + pageSize) diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/ConversationMessageService.java b/adi-common/src/main/java/com/moyz/adi/common/service/ConversationMessageService.java index c73ff1b..8f203cd 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/service/ConversationMessageService.java +++ b/adi-common/src/main/java/com/moyz/adi/common/service/ConversationMessageService.java @@ -10,19 +10,25 @@ import com.moyz.adi.common.entity.Conversation; import com.moyz.adi.common.entity.ConversationMessage; import com.moyz.adi.common.entity.User; import com.moyz.adi.common.entity.UserDayCost; +import com.moyz.adi.common.enums.ChatMessageRoleEnum; import com.moyz.adi.common.enums.ErrorEnum; import com.moyz.adi.common.exception.BaseException; import com.moyz.adi.common.helper.OpenAiHelper; import com.moyz.adi.common.helper.QuotaHelper; import com.moyz.adi.common.helper.RateLimitHelper; import com.moyz.adi.common.mapper.ConversationMessageMapper; -import com.moyz.adi.common.model.AnswerMeta; -import com.moyz.adi.common.model.QuestionMeta; import com.moyz.adi.common.util.LocalCache; import com.moyz.adi.common.util.LocalDateTimeUtil; import com.moyz.adi.common.util.UserUtil; -import com.theokanning.openai.completion.chat.ChatMessage; +import com.moyz.adi.common.vo.AnswerMeta; +import com.moyz.adi.common.vo.QuestionMeta; +import com.moyz.adi.common.vo.SseAskParams; import com.theokanning.openai.completion.chat.ChatMessageRole; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.memory.ChatMemory; +import dev.langchain4j.memory.chat.TokenWindowChatMemory; +import dev.langchain4j.model.openai.OpenAiTokenizer; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -35,12 +41,12 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; import java.text.MessageFormat; -import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.concurrent.TimeUnit; import static com.moyz.adi.common.enums.ErrorEnum.B_MESSAGE_NOT_FOUND; +import static dev.langchain4j.model.openai.OpenAiModelName.GPT_3_5_TURBO; @Slf4j @Service @@ -90,7 +96,7 @@ public class ConversationMessageService extends ServiceImpl= convsMax) { @@ -175,12 +181,16 @@ public class ConversationMessageService extends ServiceImpl chatMessageList = new ArrayList<>(); //system message Conversation conversation = conversationService.lambdaQuery() .eq(Conversation::getUuid, askReq.getConversationUuid()) @@ -188,8 +198,7 @@ public class ConversationMessageService extends ServiceImpl 0) { @@ -200,19 +209,23 @@ public class ConversationMessageService extends ServiceImpl { + openAiHelper.sseAsk(sseAskParams, (response, questionMeta, answerMeta) -> { try { _this.saveAfterAiResponse(user, askReq, response, questionMeta, answerMeta); } catch (Exception e) { @@ -228,7 +241,7 @@ public class ConversationMessageService extends ServiceImpl listByUser() { List list = this.lambdaQuery() .eq(Conversation::getUserId, ThreadContext.getCurrentUserId()) - .eq(Conversation::getIsDelete, false) + .eq(Conversation::getIsDeleted, false) .orderByDesc(Conversation::getId) .last("limit " + sysConfigService.getConversationMaxNum()) .list(); @@ -61,7 +61,7 @@ public class ConversationService extends ServiceImpl childMessages = conversationMessageService .lambdaQuery() .in(ConversationMessage::getParentMessageId, parentIds) - .eq(ConversationMessage::getIsDelete, false) + .eq(ConversationMessage::getIsDeleted, false) .list(); Map> idToMessages = childMessages.stream().collect(Collectors.groupingBy(ConversationMessage::getParentMessageId)); @@ -126,7 +126,7 @@ public class ConversationService extends ServiceImpl { @Value("${local.tmp_images}") private String tmpImagesPath; - public String writeToLocal(MultipartFile file) { + public AdiFile writeToLocal(MultipartFile file) { String md5 = MD5Utils.md5ByMultipartFile(file); Optional existFile = this.lambdaQuery() .eq(AdiFile::getMd5, md5) - .eq(AdiFile::getIsDelete, false) + .eq(AdiFile::getIsDeleted, false) .oneOpt(); if (existFile.isPresent()) { - return existFile.get().getUuid(); + return existFile.get(); } String uuid = UUID.randomUUID().toString().replace("-", ""); Pair originalFile = FileUtil.saveToLocal(file, imagePath, uuid); @@ -56,7 +56,7 @@ public class FileService extends ServiceImpl { adiFile.setExt(originalFile.getRight()); adiFile.setUserId(ThreadContext.getCurrentUserId()); this.getBaseMapper().insert(adiFile); - return uuid; + return adiFile; } public String saveToLocal(User user, String sourceImageUrl) { @@ -83,7 +83,7 @@ public class FileService extends ServiceImpl { return this.lambdaUpdate() .eq(AdiFile::getUserId, ThreadContext.getCurrentUserId()) .eq(AdiFile::getUuid, uuid) - .set(AdiFile::getIsDelete, true) + .set(AdiFile::getIsDeleted, true) .update(); } diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/Initializer.java b/adi-common/src/main/java/com/moyz/adi/common/service/Initializer.java new file mode 100644 index 0000000..de97cc2 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/service/Initializer.java @@ -0,0 +1,22 @@ +package com.moyz.adi.common.service; + +import com.moyz.adi.common.helper.EmbeddingHelper; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +@Service +public class Initializer { + + @Resource + private SysConfigService sysConfigService; + + @Resource + private EmbeddingHelper embeddingHelper; + + @PostConstruct + public void init(){ + sysConfigService.reload(); + embeddingHelper.init(); + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/KnowledgeBaseEmbeddingService.java b/adi-common/src/main/java/com/moyz/adi/common/service/KnowledgeBaseEmbeddingService.java new file mode 100644 index 0000000..ea8cba4 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/service/KnowledgeBaseEmbeddingService.java @@ -0,0 +1,29 @@ +package com.moyz.adi.common.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.moyz.adi.common.dto.KbItemEmbeddingDto; +import com.moyz.adi.common.entity.KnowledgeBaseEmbedding; +import com.moyz.adi.common.mapper.KnowledgeBaseEmbeddingMapper; +import com.moyz.adi.common.util.MPPageUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class KnowledgeBaseEmbeddingService extends ServiceImpl { + + public Page listByItemUuid(String kbItemUuid, int currentPage, int pageSize) { + Page sourcePage = baseMapper.selectByItemUuid(new Page<>(currentPage, pageSize), kbItemUuid); + Page result = new Page<>(); + MPPageUtil.convertTo(sourcePage, result, KbItemEmbeddingDto.class, (source, target) -> { + target.setEmbedding(source.getEmbedding().toArray()); + return target; + }); + return result; + } + + public boolean deleteByItemUuid(String kbItemUuid){ + return baseMapper.deleteByItemUuid(kbItemUuid); + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/KnowledgeBaseItemService.java b/adi-common/src/main/java/com/moyz/adi/common/service/KnowledgeBaseItemService.java new file mode 100644 index 0000000..4f1944f --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/service/KnowledgeBaseItemService.java @@ -0,0 +1,152 @@ +package com.moyz.adi.common.service; + +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers; +import com.moyz.adi.common.base.ThreadContext; +import com.moyz.adi.common.dto.KbItemEditReq; +import com.moyz.adi.common.entity.KnowledgeBase; +import com.moyz.adi.common.entity.KnowledgeBaseItem; +import com.moyz.adi.common.entity.User; +import com.moyz.adi.common.exception.BaseException; +import com.moyz.adi.common.helper.EmbeddingHelper; +import com.moyz.adi.common.mapper.KnowledgeBaseItemMapper; +import dev.langchain4j.data.document.Document; +import dev.langchain4j.data.document.Metadata; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; +import java.util.UUID; + +import static com.moyz.adi.common.enums.ErrorEnum.*; + +@Slf4j +@Service +public class KnowledgeBaseItemService extends ServiceImpl { + + @Resource + private EmbeddingHelper embeddingHelper; + + @Resource + private KnowledgeBaseEmbeddingService knowledgeBaseEmbeddingService; + + @Lazy + @Resource + private KnowledgeBaseService knowledgeBaseService; + + public KnowledgeBaseItem saveOrUpdate(KbItemEditReq itemEditReq) { + String uuid = itemEditReq.getUuid(); + KnowledgeBaseItem item = new KnowledgeBaseItem(); + item.setTitle(itemEditReq.getTitle()); + if (StringUtils.isNotBlank(itemEditReq.getBrief())) { + item.setBrief(itemEditReq.getBrief()); + } else { + item.setBrief(StringUtils.substring(itemEditReq.getRemark(), 0, 200)); + } + item.setRemark(itemEditReq.getRemark()); + if (null == itemEditReq.getId() || itemEditReq.getId() < 1) { + uuid = UUID.randomUUID().toString().replace("-", ""); + item.setUuid(uuid); + item.setKbId(itemEditReq.getKbId()); + item.setKbUuid(itemEditReq.getKbUuid()); + baseMapper.insert(item); + } else { + item.setId(itemEditReq.getId()); + baseMapper.updateById(item); + } + return ChainWrappers.lambdaQueryChain(baseMapper) + .eq(KnowledgeBaseItem::getUuid, uuid) + .one(); + } + + public KnowledgeBaseItem getEnable(String uuid) { + return ChainWrappers.lambdaQueryChain(baseMapper) + .eq(KnowledgeBaseItem::getUuid, uuid) + .eq(KnowledgeBaseItem::getIsDeleted, false) + .one(); + } + + public Page search(String kbUuid, String keyword, Integer currentPage, Integer pageSize) { + LambdaQueryChainWrapper wrapper = ChainWrappers.lambdaQueryChain(baseMapper); + wrapper.select(KnowledgeBaseItem::getId, KnowledgeBaseItem::getUuid, KnowledgeBaseItem::getTitle, KnowledgeBaseItem::getBrief, KnowledgeBaseItem::getKbUuid, KnowledgeBaseItem::getIsEmbedded, KnowledgeBaseItem::getCreateTime, KnowledgeBaseItem::getUpdateTime); + wrapper.eq(KnowledgeBaseItem::getIsDeleted, false); + wrapper.eq(KnowledgeBaseItem::getKbUuid, kbUuid); + if (StringUtils.isNotBlank(keyword)) { + wrapper.eq(KnowledgeBaseItem::getTitle, keyword); + } + return wrapper.page(new Page<>(currentPage, pageSize)); + } + + public boolean checkAndEmbedding(String[] uuids) { + if (ArrayUtils.isEmpty(uuids)) { + return false; + } + for (String uuid : uuids) { + checkAndEmbedding(uuid); + } + return true; + } + + public boolean checkAndEmbedding(String uuid) { + if (checkPrivilege(uuid)) { + KnowledgeBaseItem one = getEnable(uuid); + return embedding(one); + } + return false; + } + + + public boolean embedding(KnowledgeBaseItem one) { + Metadata metadata = new Metadata(); + metadata.add("kb_uuid", one.getKbUuid()); + metadata.add("kb_item_uuid", one.getUuid()); + Document document = new Document(one.getRemark(), metadata); + embeddingHelper.getEmbeddingStoreIngestor().ingest(document); + return true; + } + + @Transactional + public boolean softDelete(String uuid) { + boolean privilege = checkPrivilege(uuid); + if (!privilege) throw new BaseException(A_USER_NOT_AUTH); + boolean success = ChainWrappers.lambdaUpdateChain(baseMapper) + .eq(KnowledgeBaseItem::getUuid, uuid) + .set(KnowledgeBaseItem::getIsDeleted, true) + .update(); + if (!success) { + return false; + } + knowledgeBaseEmbeddingService.deleteByItemUuid(uuid); + return true; + } + + private boolean checkPrivilege(String uuid) { + if (StringUtils.isBlank(uuid)) { + throw new BaseException(A_PARAMS_ERROR); + } + User user = ThreadContext.getCurrentUser(); + if (null == user) { + throw new BaseException(A_USER_NOT_EXIST); + } + if (user.getIsAdmin()) { + return true; + } + Optional kbItem = ChainWrappers.lambdaQueryChain(baseMapper) + .eq(KnowledgeBaseItem::getUuid, uuid) + .oneOpt(); + if (kbItem.isPresent()) { + KnowledgeBase kb = knowledgeBaseService.getById(kbItem.get().getKbId()); + if (null != kb) { + return kb.getOwnerId().equals(user.getId()); + } + } + return false; + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/KnowledgeBaseQaRecordService.java b/adi-common/src/main/java/com/moyz/adi/common/service/KnowledgeBaseQaRecordService.java new file mode 100644 index 0000000..f941c82 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/service/KnowledgeBaseQaRecordService.java @@ -0,0 +1,52 @@ +package com.moyz.adi.common.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers; +import com.moyz.adi.common.base.ThreadContext; +import com.moyz.adi.common.entity.KnowledgeBaseQaRecord; +import com.moyz.adi.common.exception.BaseException; +import com.moyz.adi.common.mapper.KnowledgeBaseQaRecordMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import static com.moyz.adi.common.enums.ErrorEnum.A_DATA_NOT_FOUND; + +@Slf4j +@Service +public class KnowledgeBaseQaRecordService extends ServiceImpl { + + public Page search(String kbUuid, String keyword, Integer currentPage, Integer pageSize) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(KnowledgeBaseQaRecord::getKbUuid, kbUuid); + if (!ThreadContext.getCurrentUser().getIsAdmin()) { + wrapper.eq(KnowledgeBaseQaRecord::getUserId, ThreadContext.getCurrentUserId()); + } + if (StringUtils.isNotBlank(keyword)) { + wrapper.like(KnowledgeBaseQaRecord::getQuestion, keyword); + } + wrapper.orderByDesc(KnowledgeBaseQaRecord::getUpdateTime); + return baseMapper.selectPage(new Page<>(currentPage, pageSize), wrapper); + } + + public boolean softDelele(String uuid) { + if (ThreadContext.getCurrentUser().getIsAdmin()) { + return ChainWrappers.lambdaUpdateChain(baseMapper) + .eq(KnowledgeBaseQaRecord::getUuid, uuid) + .set(KnowledgeBaseQaRecord::getIsDeleted, true) + .update(); + } + KnowledgeBaseQaRecord exist = ChainWrappers.lambdaQueryChain(baseMapper) + .eq(KnowledgeBaseQaRecord::getUuid, uuid) + .one(); + if (null == exist) { + throw new BaseException(A_DATA_NOT_FOUND); + } + return ChainWrappers.lambdaUpdateChain(baseMapper) + .eq(KnowledgeBaseQaRecord::getId, exist.getId()) + .set(KnowledgeBaseQaRecord::getIsDeleted, true) + .update(); + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/KnowledgeBaseService.java b/adi-common/src/main/java/com/moyz/adi/common/service/KnowledgeBaseService.java new file mode 100644 index 0000000..6f3eacc --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/service/KnowledgeBaseService.java @@ -0,0 +1,249 @@ +package com.moyz.adi.common.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers; +import com.moyz.adi.common.base.ThreadContext; +import com.moyz.adi.common.cosntant.RedisKeyConstant; +import com.moyz.adi.common.dto.KbEditReq; +import com.moyz.adi.common.entity.*; +import com.moyz.adi.common.exception.BaseException; +import com.moyz.adi.common.helper.EmbeddingHelper; +import com.moyz.adi.common.mapper.KnowledgeBaseMapper; +import com.moyz.adi.common.util.BizPager; +import com.moyz.adi.common.util.LocalDateTimeUtil; +import dev.langchain4j.data.document.Document; +import dev.langchain4j.data.document.parser.TextDocumentParser; +import dev.langchain4j.data.document.parser.apache.pdfbox.ApachePdfBoxDocumentParser; +import dev.langchain4j.data.document.parser.apache.poi.ApachePoiDocumentParser; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.text.MessageFormat; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static com.moyz.adi.common.cosntant.AdiConstant.POI_DOC_TYPES; +import static com.moyz.adi.common.enums.ErrorEnum.*; +import static dev.langchain4j.data.document.loader.FileSystemDocumentLoader.loadDocument; + +@Slf4j +@Service +public class KnowledgeBaseService extends ServiceImpl { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + @Resource + private EmbeddingHelper embeddingHelper; + + @Resource + private KnowledgeBaseItemService knowledgeBaseItemService; + + @Resource + private KnowledgeBaseQaRecordService knowledgeBaseQaRecordService; + + @Resource + private FileService fileService; + + public KnowledgeBase saveOrUpdate(KbEditReq kbEditReq) { + String uuid = kbEditReq.getUuid(); + KnowledgeBase knowledgeBase = new KnowledgeBase(); + knowledgeBase.setTitle(kbEditReq.getTitle()); + knowledgeBase.setRemark(kbEditReq.getRemark()); + if (null != kbEditReq.getIsPublic()) { + knowledgeBase.setIsPublic(kbEditReq.getIsPublic()); + } + if (null == kbEditReq.getId() || kbEditReq.getId() < 1) { + User user = ThreadContext.getCurrentUser(); + uuid = UUID.randomUUID().toString().replace("-", ""); + knowledgeBase.setUuid(uuid); + knowledgeBase.setOwnerId(user.getId()); + knowledgeBase.setOwnerName(user.getName()); + baseMapper.insert(knowledgeBase); + } else { + knowledgeBase.setId(kbEditReq.getId()); + baseMapper.updateById(knowledgeBase); + } + return ChainWrappers.lambdaQueryChain(baseMapper) + .eq(KnowledgeBase::getUuid, uuid) + .one(); + } + + public List uploadDocs(String kbUuid, Boolean embedding, MultipartFile[] docs) { + if (ArrayUtils.isEmpty(docs)) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + KnowledgeBase knowledgeBase = ChainWrappers.lambdaQueryChain(baseMapper) + .eq(KnowledgeBase::getUuid, kbUuid) + .eq(KnowledgeBase::getIsDeleted, false) + .oneOpt() + .orElseThrow(() -> new BaseException(A_DATA_NOT_FOUND)); + for (MultipartFile doc : docs) { + try { + result.add(uploadDoc(knowledgeBase, doc, embedding)); + } catch (Exception e) { + log.warn("uploadDocs fail,fileName:{}", doc.getOriginalFilename(), e); + } + } + return result; + } + + public AdiFile uploadDoc(String kbUuid, Boolean embedding, MultipartFile doc) { + KnowledgeBase knowledgeBase = ChainWrappers.lambdaQueryChain(baseMapper) + .eq(KnowledgeBase::getUuid, kbUuid) + .eq(KnowledgeBase::getIsDeleted, false) + .oneOpt() + .orElseThrow(() -> new BaseException(A_DATA_NOT_FOUND)); + return uploadDoc(knowledgeBase, doc, embedding); + } + + private AdiFile uploadDoc(KnowledgeBase knowledgeBase, MultipartFile doc, Boolean embedding) { + try { + String fileName = doc.getOriginalFilename(); + AdiFile adiFile = fileService.writeToLocal(doc); + + //解析文档 + Document document; + if (adiFile.getExt().equalsIgnoreCase("txt")) { + document = loadDocument(adiFile.getPath(), new TextDocumentParser()); + } else if (adiFile.getExt().equalsIgnoreCase("pdf")) { + document = loadDocument(adiFile.getPath(), new ApachePdfBoxDocumentParser()); + } else if (ArrayUtils.contains(POI_DOC_TYPES, adiFile.getExt())) { + document = loadDocument(adiFile.getPath(), new ApachePoiDocumentParser()); + } else { + log.warn("该文件类型:{}无法解析,忽略", adiFile.getExt()); + return adiFile; + } + //创建知识库条目 + String uuid = UUID.randomUUID().toString().replace("-", ""); + KnowledgeBaseItem knowledgeBaseItem = new KnowledgeBaseItem(); + knowledgeBaseItem.setUuid(uuid); + knowledgeBaseItem.setKbId(knowledgeBase.getId()); + knowledgeBaseItem.setKbUuid(knowledgeBase.getUuid()); + knowledgeBaseItem.setSourceFileId(adiFile.getId()); + knowledgeBaseItem.setTitle(fileName); + knowledgeBaseItem.setBrief(StringUtils.substring(document.text(), 0, 200)); + knowledgeBaseItem.setRemark(document.text()); + knowledgeBaseItem.setIsEmbedded(true); + boolean success = knowledgeBaseItemService.save(knowledgeBaseItem); + if (success && Boolean.TRUE.equals(embedding)) { + knowledgeBaseItem = knowledgeBaseItemService.getEnable(uuid); + + //向量化 + Document docWithoutPath = new Document(document.text()); + docWithoutPath.metadata() + .add("kb_uuid", knowledgeBase.getUuid()) + .add("kb_item_uuid", knowledgeBaseItem.getUuid()); + + embeddingHelper.getEmbeddingStoreIngestor().ingest(docWithoutPath); + + knowledgeBaseItemService + .lambdaUpdate() + .eq(KnowledgeBaseItem::getId, knowledgeBaseItem.getId()) + .set(KnowledgeBaseItem::getIsEmbedded, true) + .update(); + } + return adiFile; + } catch (Exception e) { + log.error("upload error", e); + throw new BaseException(A_UPLOAD_FAIL); + } + } + + public boolean embedding(String kbUuid, boolean forceAll) { + boolean privilege = checkPrivilege(null, kbUuid); + if (!privilege) throw new BaseException(A_USER_NOT_AUTH); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); + wrapper.eq(KnowledgeBaseItem::getIsDeleted, false); + wrapper.eq(KnowledgeBaseItem::getUuid, kbUuid); + BizPager.oneByOneWithAnchor(wrapper, knowledgeBaseItemService, KnowledgeBaseItem::getId, one -> { + if (forceAll || !one.getIsEmbedded()) { + knowledgeBaseItemService.embedding(one); + } + }); + return true; + } + + public Page search(String keyword, Boolean includeOthersPublic, Integer currentPage, Integer pageSize) { + User user = ThreadContext.getCurrentUser(); + if (user.getIsAdmin()) { + return baseMapper.searchByAdmin(new Page<>(currentPage, pageSize), keyword); + } else { + return baseMapper.searchByUser(new Page<>(currentPage, pageSize), user.getId(), keyword, includeOthersPublic); + } + } + + + public boolean softDelete(String uuid) { + boolean privs = checkPrivilege(null, uuid); + if (!privs) throw new BaseException(A_USER_NOT_AUTH); + return ChainWrappers.lambdaUpdateChain(baseMapper) + .eq(KnowledgeBase::getUuid, uuid) + .set(KnowledgeBase::getIsDeleted, true) + .update(); + } + + public KnowledgeBaseQaRecord answerAndRecord(String kbUuid, String question) { + + String key = MessageFormat.format(RedisKeyConstant.AQ_ASK_TIMES, ThreadContext.getCurrentUserId(), LocalDateTimeUtil.format(LocalDateTime.now(), "yyyyMMdd")); + String askTimes = stringRedisTemplate.opsForValue().get(key); + String askQuota = SysConfigService.getByKey("quota_by_qa_ask_daily"); + if (null != askTimes && null != askQuota && Integer.parseInt(askTimes) >= Integer.parseInt(askQuota)) { + throw new BaseException(A_QA_ASK_LIMIT); + } + stringRedisTemplate.opsForValue().increment(key); + + KnowledgeBase knowledgeBase = getOrThrow(kbUuid); + String answer = embeddingHelper.findAnswer(kbUuid, question); + String uuid = UUID.randomUUID().toString().replace("-", ""); + KnowledgeBaseQaRecord newObj = new KnowledgeBaseQaRecord(); + newObj.setKbId(knowledgeBase.getId()); + newObj.setKbUuid((knowledgeBase.getUuid())); + newObj.setUuid(uuid); + newObj.setUserId(ThreadContext.getCurrentUserId()); + newObj.setQuestion(question); + newObj.setAnswer(answer); + knowledgeBaseQaRecordService.save(newObj); + return knowledgeBaseQaRecordService.lambdaQuery().eq(KnowledgeBaseQaRecord::getUuid, uuid).one(); + } + + public KnowledgeBase getOrThrow(String kbUuid) { + return ChainWrappers.lambdaQueryChain(baseMapper) + .eq(KnowledgeBase::getUuid, kbUuid) + .eq(KnowledgeBase::getIsDeleted, false) + .oneOpt().orElseThrow(() -> new BaseException(A_DATA_NOT_FOUND)); + } + + private boolean checkPrivilege(Long kbId, String kbUuid) { + if (null == kbId && StringUtils.isBlank(kbUuid)) { + throw new BaseException(A_PARAMS_ERROR); + } + User user = ThreadContext.getCurrentUser(); + if (null == user) { + throw new BaseException(A_USER_NOT_EXIST); + } + boolean privilege = user.getIsAdmin(); + if (privilege) { + return true; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); + wrapper.eq(KnowledgeBase::getOwnerId, user.getId()); + if (null != kbId) { + wrapper = wrapper.eq(KnowledgeBase::getId, kbId); + } else if (StringUtils.isNotBlank(kbUuid)) { + wrapper = wrapper.eq(KnowledgeBase::getUuid, kbUuid); + } + return baseMapper.exists(wrapper); + } +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/PromptService.java b/adi-common/src/main/java/com/moyz/adi/common/service/PromptService.java index 998117b..b5371e6 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/service/PromptService.java +++ b/adi-common/src/main/java/com/moyz/adi/common/service/PromptService.java @@ -25,7 +25,7 @@ import java.util.Map; public class PromptService extends ServiceImpl { public List getAll(long userId) { - List prompts = this.lambdaQuery().eq(Prompt::getUserId, userId).eq(Prompt::getIsDelete, false).list(); + List prompts = this.lambdaQuery().eq(Prompt::getUserId, userId).eq(Prompt::getIsDeleted, false).list(); return MPPageUtil.convertTo(prompts, PromptDto.class); } @@ -34,13 +34,13 @@ public class PromptService extends ServiceImpl { if (StringUtils.isNotBlank(keyword)) { promptPage = this.lambdaQuery() .eq(Prompt::getUserId, ThreadContext.getCurrentUserId()) - .eq(Prompt::getIsDelete, false) + .eq(Prompt::getIsDeleted, false) .like(Prompt::getAct, keyword) .page(new Page<>(currentPage, pageSize)); } else { promptPage = this.lambdaQuery() .eq(Prompt::getUserId, ThreadContext.getCurrentUserId()) - .eq(Prompt::getIsDelete, false) + .eq(Prompt::getIsDeleted, false) .page(new Page<>(currentPage, pageSize)); } return MPPageUtil.convertTo(promptPage, new Page<>(), PromptDto.class); @@ -51,14 +51,14 @@ public class PromptService extends ServiceImpl { if (StringUtils.isNotBlank(keyword)) { promptPage = this.lambdaQuery() .eq(Prompt::getUserId, ThreadContext.getCurrentUserId()) - .eq(Prompt::getIsDelete, false) + .eq(Prompt::getIsDeleted, false) .like(Prompt::getAct, keyword) .last("limit 10") .list(); } else { promptPage = this.lambdaQuery() .eq(Prompt::getUserId, ThreadContext.getCurrentUserId()) - .eq(Prompt::getIsDelete, false) + .eq(Prompt::getIsDeleted, false) .last("limit 10") .list(); } @@ -106,7 +106,7 @@ public class PromptService extends ServiceImpl { Prompt existOne = this.lambdaQuery() .eq(Prompt::getUserId, userId) .eq(Prompt::getAct, title) - .eq(Prompt::getIsDelete, false) + .eq(Prompt::getIsDeleted, false) .one(); if (null != existOne) { //modify @@ -137,14 +137,14 @@ public class PromptService extends ServiceImpl { Prompt prompt = this.lambdaQuery() .eq(Prompt::getUserId, ThreadContext.getCurrentUserId()) .eq(Prompt::getId, id) - .eq(Prompt::getIsDelete, false) + .eq(Prompt::getIsDeleted, false) .one(); if (null == prompt) { return false; } Prompt updateOne = new Prompt(); updateOne.setId(id); - updateOne.setIsDelete(true); + updateOne.setIsDeleted(true); return this.updateById(updateOne); } @@ -152,7 +152,7 @@ public class PromptService extends ServiceImpl { Prompt prompt = this.lambdaQuery() .eq(Prompt::getId, id) .eq(Prompt::getUserId, ThreadContext.getCurrentUserId()) - .eq(Prompt::getIsDelete, false) + .eq(Prompt::getIsDeleted, false) .one(); if (null == prompt) { return false; diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/SysConfigService.java b/adi-common/src/main/java/com/moyz/adi/common/service/SysConfigService.java index 0ded621..21f58ca 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/service/SysConfigService.java +++ b/adi-common/src/main/java/com/moyz/adi/common/service/SysConfigService.java @@ -1,13 +1,16 @@ package com.moyz.adi.common.service; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.moyz.adi.common.cosntant.AdiConstant; -import com.moyz.adi.common.model.RequestRateLimit; -import com.moyz.adi.common.util.JsonUtil; -import com.moyz.adi.common.util.LocalCache; import com.moyz.adi.common.entity.SysConfig; import com.moyz.adi.common.mapper.SysConfigMapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.moyz.adi.common.util.JsonUtil; +import com.moyz.adi.common.util.LocalCache; +import com.moyz.adi.common.vo.RequestRateLimit; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -21,7 +24,7 @@ public class SysConfigService extends ServiceImpl { @Scheduled(fixedDelay = 20 * 60 * 1000) public void reload() { log.info("reload system config"); - List configsFromDB = this.lambdaQuery().eq(SysConfig::getIsDelete, false).list(); + List configsFromDB = this.lambdaQuery().eq(SysConfig::getIsDeleted, false).list(); if (LocalCache.CONFIGS.isEmpty()) { configsFromDB.stream().forEach(item -> LocalCache.CONFIGS.put(item.getName(), item.getValue())); } else { @@ -58,4 +61,24 @@ public class SysConfigService extends ServiceImpl { return LocalCache.CONFIGS.get(AdiConstant.SysConfigKey.SECRET_KEY); } + public static String getByKey(String key) { + return LocalCache.CONFIGS.get(key); + } + + public static Integer getIntByKey(String key) { + String val = LocalCache.CONFIGS.get(key); + if (null != val) { + return Integer.parseInt(val); + } + return null; + } + + public Page search(String keyword, Integer currentPage, Integer pageSize) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (StringUtils.isNotBlank(keyword)) { + wrapper.eq(SysConfig::getName, keyword); + } + return baseMapper.selectPage(new Page<>(currentPage, pageSize), wrapper); + } + } diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/UserDayCostService.java b/adi-common/src/main/java/com/moyz/adi/common/service/UserDayCostService.java index d9d3c51..b66b081 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/service/UserDayCostService.java +++ b/adi-common/src/main/java/com/moyz/adi/common/service/UserDayCostService.java @@ -5,7 +5,7 @@ import com.moyz.adi.common.entity.User; import com.moyz.adi.common.util.LocalDateTimeUtil; import com.moyz.adi.common.entity.UserDayCost; import com.moyz.adi.common.mapper.UserDayCostMapper; -import com.moyz.adi.common.model.CostStat; +import com.moyz.adi.common.vo.CostStat; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.moyz.adi.common.util.UserUtil; import lombok.extern.slf4j.Slf4j; diff --git a/adi-common/src/main/java/com/moyz/adi/common/service/UserService.java b/adi-common/src/main/java/com/moyz/adi/common/service/UserService.java index 062401d..a925635 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/service/UserService.java +++ b/adi-common/src/main/java/com/moyz/adi/common/service/UserService.java @@ -3,22 +3,22 @@ package com.moyz.adi.common.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers; -import com.moyz.adi.common.cosntant.AdiConstant; -import com.moyz.adi.common.enums.ErrorEnum; -import com.moyz.adi.common.enums.UserStatusEnum; -import com.moyz.adi.common.exception.BaseException; -import com.moyz.adi.common.util.JsonUtil; -import com.moyz.adi.common.util.LocalCache; import com.moyz.adi.common.base.ThreadContext; +import com.moyz.adi.common.cosntant.AdiConstant; import com.moyz.adi.common.cosntant.RedisKeyConstant; import com.moyz.adi.common.dto.ConfigResp; import com.moyz.adi.common.dto.LoginReq; import com.moyz.adi.common.dto.LoginResp; import com.moyz.adi.common.dto.UserUpdateReq; import com.moyz.adi.common.entity.User; +import com.moyz.adi.common.enums.ErrorEnum; +import com.moyz.adi.common.enums.UserStatusEnum; +import com.moyz.adi.common.exception.BaseException; import com.moyz.adi.common.helper.AdiMailSender; import com.moyz.adi.common.mapper.UserMapper; -import com.moyz.adi.common.model.CostStat; +import com.moyz.adi.common.util.JsonUtil; +import com.moyz.adi.common.util.LocalCache; +import com.moyz.adi.common.vo.CostStat; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -71,7 +71,7 @@ public class UserService extends ServiceImpl { } return this.lambdaQuery() .eq(User::getEmail, email) - .eq(User::getIsDelete, false) + .eq(User::getIsDeleted, false) .oneOpt() .orElseThrow(() -> new BaseException(A_USER_NOT_EXIST)); } @@ -94,7 +94,7 @@ public class UserService extends ServiceImpl { stringRedisTemplate.delete(captchaInCache); User user = ChainWrappers.lambdaQueryChain(baseMapper) - .eq(User::getIsDelete, false) + .eq(User::getIsDeleted, false) .eq(User::getEmail, email) .one(); if (null != user && user.getUserStatus() == UserStatusEnum.NORMAL) { @@ -112,7 +112,7 @@ public class UserService extends ServiceImpl { //创建用户 User newOne = new User(); - newOne.setName(email.substring(0, email.indexOf("@"))); + newOne.setName(StringUtils.substringBetween(email, "@")); newOne.setUuid(UUID.randomUUID().toString().replace("-", "")); newOne.setEmail(email); newOne.setPassword(hashed); @@ -157,7 +157,7 @@ public class UserService extends ServiceImpl { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); User user = this.lambdaQuery() .eq(User::getEmail, email) - .eq(User::getIsDelete, false) + .eq(User::getIsDeleted, false) .oneOpt() .orElse(null); if (null == user) { @@ -201,7 +201,7 @@ public class UserService extends ServiceImpl { //captcha check end User user = this.lambdaQuery() - .eq(User::getIsDelete, false) + .eq(User::getIsDeleted, false) .eq(User::getEmail, loginReq.getEmail()) .oneOpt() .orElseThrow(() -> new BaseException(ErrorEnum.A_USER_NOT_EXIST)); diff --git a/adi-common/src/main/java/com/moyz/adi/common/util/AdiPgVectorEmbeddingStore.java b/adi-common/src/main/java/com/moyz/adi/common/util/AdiPgVectorEmbeddingStore.java new file mode 100644 index 0000000..1d20bfe --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/util/AdiPgVectorEmbeddingStore.java @@ -0,0 +1,341 @@ +package com.moyz.adi.common.util; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.pgvector.PGvector; +import dev.langchain4j.data.document.Metadata; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.store.embedding.EmbeddingMatch; +import dev.langchain4j.store.embedding.EmbeddingStore; +import lombok.Builder; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Type; +import java.sql.*; +import java.util.*; + +import static dev.langchain4j.internal.Utils.*; +import static dev.langchain4j.internal.ValidationUtils.*; +import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.toList; + +/** + * 复制并做了少许改动() + * PGVector EmbeddingStore Implementation + *

+ * Only cosine similarity is used. + * Only ivfflat index is used. + */ +public class AdiPgVectorEmbeddingStore implements EmbeddingStore { + + private static final Logger log = LoggerFactory.getLogger(AdiPgVectorEmbeddingStore.class); + + private static final Gson GSON = new Gson(); + + private final String host; + private final Integer port; + private final String user; + private final String password; + private final String database; + private final String table; + + /** + * All args constructor for PgVectorEmbeddingStore Class + * + * @param host The database host + * @param port The database port + * @param user The database user + * @param password The database password + * @param database The database name + * @param table The database table + * @param dimension The vector dimension + * @param useIndex Should use IVFFlat index + * @param indexListSize The IVFFlat number of lists + * @param createTable Should create table automatically + * @param dropTableFirst Should drop table first, usually for testing + */ + @Builder + public AdiPgVectorEmbeddingStore( + String host, + Integer port, + String user, + String password, + String database, + String table, + Integer dimension, + Boolean useIndex, + Integer indexListSize, + Boolean createTable, + Boolean dropTableFirst) { + this.host = ensureNotBlank(host, "host"); + this.port = ensureGreaterThanZero(port, "port"); + this.user = ensureNotBlank(user, "user"); + this.password = ensureNotBlank(password, "password"); + this.database = ensureNotBlank(database, "database"); + this.table = ensureNotBlank(table, "table"); + + useIndex = getOrDefault(useIndex, false); + createTable = getOrDefault(createTable, true); + dropTableFirst = getOrDefault(dropTableFirst, false); + + try (Connection connection = setupConnection()) { + + if (dropTableFirst) { + connection.createStatement().executeUpdate(String.format("DROP TABLE IF EXISTS %s", table)); + } + + if (createTable) { + connection.createStatement().executeUpdate(String.format( + "CREATE TABLE IF NOT EXISTS %s (" + + "embedding_id UUID PRIMARY KEY, " + + "embedding vector(%s), " + + "text TEXT NULL, " + + "metadata JSON NULL" + + ")", + table, ensureGreaterThanZero(dimension, "dimension"))); + } + + if (useIndex) { + final String indexName = table + "_ivfflat_index"; + connection.createStatement().executeUpdate(String.format( + "CREATE INDEX IF NOT EXISTS %s ON %s " + + "USING ivfflat (embedding vector_cosine_ops) " + + "WITH (lists = %s)", + indexName, table, ensureGreaterThanZero(indexListSize, "indexListSize"))); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private Connection setupConnection() throws SQLException { + Connection connection = DriverManager.getConnection( + String.format("jdbc:postgresql://%s:%s/%s", host, port, database), + user, + password + ); + connection.createStatement().executeUpdate("CREATE EXTENSION IF NOT EXISTS vector"); + PGvector.addVectorType(connection); + return connection; + } + + /** + * Adds a given embedding to the store. + * + * @param embedding The embedding to be added to the store. + * @return The auto-generated ID associated with the added embedding. + */ + @Override + public String add(Embedding embedding) { + String id = randomUUID(); + addInternal(id, embedding, null); + return id; + } + + /** + * Adds a given embedding to the store. + * + * @param id The unique identifier for the embedding to be added. + * @param embedding The embedding to be added to the store. + */ + @Override + public void add(String id, Embedding embedding) { + addInternal(id, embedding, null); + } + + /** + * Adds a given embedding and the corresponding content that has been embedded to the store. + * + * @param embedding The embedding to be added to the store. + * @param textSegment Original content that was embedded. + * @return The auto-generated ID associated with the added embedding. + */ + @Override + public String add(Embedding embedding, TextSegment textSegment) { + String id = randomUUID(); + addInternal(id, embedding, textSegment); + return id; + } + + /** + * Adds multiple embeddings to the store. + * + * @param embeddings A list of embeddings to be added to the store. + * @return A list of auto-generated IDs associated with the added embeddings. + */ + @Override + public List addAll(List embeddings) { + List ids = embeddings.stream().map(ignored -> randomUUID()).collect(toList()); + addAllInternal(ids, embeddings, null); + return ids; + } + + /** + * Adds multiple embeddings and their corresponding contents that have been embedded to the store. + * + * @param embeddings A list of embeddings to be added to the store. + * @param embedded A list of original contents that were embedded. + * @return A list of auto-generated IDs associated with the added embeddings. + */ + @Override + public List addAll(List embeddings, List embedded) { + List ids = embeddings.stream().map(ignored -> randomUUID()).collect(toList()); + addAllInternal(ids, embeddings, embedded); + return ids; + } + + /** + * Finds the most relevant (closest in space) embeddings to the provided reference embedding. + * + * @param referenceEmbedding The embedding used as a reference. Returned embeddings should be relevant (closest) to this one. + * @param maxResults The maximum number of embeddings to be returned. + * @param minScore The minimum relevance score, ranging from 0 to 1 (inclusive). + * Only embeddings with a score of this value or higher will be returned. + * @return A list of embedding matches. + * Each embedding match includes a relevance score (derivative of cosine distance), + * ranging from 0 (not relevant) to 1 (highly relevant). + */ + @Override + public List> findRelevant(Embedding referenceEmbedding, int maxResults, double minScore) { + List> result = new ArrayList<>(); + try (Connection connection = setupConnection()) { + String referenceVector = Arrays.toString(referenceEmbedding.vector()); + String query = String.format( + "WITH temp AS (SELECT (2 - (embedding <=> '%s')) / 2 AS score, embedding_id, embedding, text, metadata FROM %s) SELECT * FROM temp WHERE score >= %s ORDER BY score desc LIMIT %s;", + referenceVector, table, minScore, maxResults); + PreparedStatement selectStmt = connection.prepareStatement(query); + + ResultSet resultSet = selectStmt.executeQuery(); + while (resultSet.next()) { + double score = resultSet.getDouble("score"); + String embeddingId = resultSet.getString("embedding_id"); + + PGvector vector = (PGvector) resultSet.getObject("embedding"); + Embedding embedding = new Embedding(vector.toArray()); + + String text = resultSet.getString("text"); + TextSegment textSegment = null; + if (isNotNullOrBlank(text)) { + String metadataJson = Optional.ofNullable(resultSet.getString("metadata")).orElse("{}"); + Type type = new TypeToken>() { + }.getType(); + Metadata metadata = new Metadata(new HashMap<>(GSON.fromJson(metadataJson, type))); + textSegment = TextSegment.from(text, metadata); + } + + result.add(new EmbeddingMatch<>(score, embeddingId, embedding, textSegment)); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + return result; + } + + private void addInternal(String id, Embedding embedding, TextSegment embedded) { + addAllInternal( + singletonList(id), + singletonList(embedding), + embedded == null ? null : singletonList(embedded)); + } + + private void addAllInternal( + List ids, List embeddings, List embedded) { + if (isNullOrEmpty(ids) || isNullOrEmpty(embeddings)) { + log.info("Empty embeddings - no ops"); + return; + } + ensureTrue(ids.size() == embeddings.size(), "ids size is not equal to embeddings size"); + ensureTrue(embedded == null || embeddings.size() == embedded.size(), + "embeddings size is not equal to embedded size"); + + try (Connection connection = setupConnection()) { + String query = String.format( + "INSERT INTO %s (embedding_id, embedding, text, metadata) VALUES (?, ?, ?, ?)" + + "ON CONFLICT (embedding_id) DO UPDATE SET " + + "embedding = EXCLUDED.embedding," + + "text = EXCLUDED.text," + + "metadata = EXCLUDED.metadata;", + table); + + PreparedStatement upsertStmt = connection.prepareStatement(query); + + for (int i = 0; i < ids.size(); ++i) { + upsertStmt.setObject(1, UUID.fromString(ids.get(i))); + upsertStmt.setObject(2, new PGvector(embeddings.get(i).vector())); + + if (embedded != null && embedded.get(i) != null) { + upsertStmt.setObject(3, embedded.get(i).text()); + Map metadata = embedded.get(i).metadata().asMap(); + upsertStmt.setObject(4, GSON.toJson(metadata), Types.OTHER); + } else { + upsertStmt.setNull(3, Types.VARCHAR); + upsertStmt.setNull(4, Types.OTHER); + } + upsertStmt.addBatch(); + } + + upsertStmt.executeBatch(); + + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + + //adi + public List> findRelevantByKbUuid(String kbUuid, Embedding referenceEmbedding, int maxResults, double minScore) { + List> result = new ArrayList<>(); + try (Connection connection = setupConnection()) { + String referenceVector = Arrays.toString(referenceEmbedding.vector()); + //新增查询条件kb_id + String query = String.format( + "WITH temp AS (SELECT (2 - (embedding <=> '%s')) / 2 AS score, embedding_id, embedding, text, metadata FROM %s where metadata->>'kb_uuid' = '%s') SELECT * FROM temp WHERE score >= %s ORDER BY score desc LIMIT %s;", + referenceVector, table, kbUuid, minScore, maxResults); + PreparedStatement selectStmt = connection.prepareStatement(query); + + ResultSet resultSet = selectStmt.executeQuery(); + while (resultSet.next()) { + double score = resultSet.getDouble("score"); + String embeddingId = resultSet.getString("embedding_id"); + + PGvector vector = (PGvector) resultSet.getObject("embedding"); + Embedding embedding = new Embedding(vector.toArray()); + + String text = resultSet.getString("text"); + TextSegment textSegment = null; + if (isNotNullOrBlank(text)) { + String metadataJson = Optional.ofNullable(resultSet.getString("metadata")).orElse("{}"); + Type type = new TypeToken>() { + }.getType(); + Metadata metadata = new Metadata(new HashMap<>(GSON.fromJson(metadataJson, type))); + textSegment = TextSegment.from(text, metadata); + } + + result.add(new EmbeddingMatch<>(score, embeddingId, embedding, textSegment)); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + + return result; + } + + public int deleteByMetadata(String metadataKey, String metadataValue) { + if (StringUtils.isAnyBlank(metadataKey, metadataValue)) { + return NumberUtils.INTEGER_ZERO; + } + try (Connection connection = setupConnection()) { + String query = String.format("delete from %s where metadata->'%s'=?", table, metadataKey); + PreparedStatement prepareStatement = connection.prepareStatement(query); + prepareStatement.setString(1, metadataValue); + return prepareStatement.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/adi-common/src/main/java/com/moyz/adi/common/util/LocalCache.java b/adi-common/src/main/java/com/moyz/adi/common/util/LocalCache.java index 288f2b6..23c77a6 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/util/LocalCache.java +++ b/adi-common/src/main/java/com/moyz/adi/common/util/LocalCache.java @@ -1,6 +1,6 @@ package com.moyz.adi.common.util; -import com.moyz.adi.common.model.RequestRateLimit; +import com.moyz.adi.common.vo.RequestRateLimit; import java.util.HashMap; import java.util.Map; diff --git a/adi-common/src/main/java/com/moyz/adi/common/util/MPPageUtil.java b/adi-common/src/main/java/com/moyz/adi/common/util/MPPageUtil.java index 68eba5c..a19f47b 100644 --- a/adi-common/src/main/java/com/moyz/adi/common/util/MPPageUtil.java +++ b/adi-common/src/main/java/com/moyz/adi/common/util/MPPageUtil.java @@ -9,11 +9,16 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.BiFunction; @Slf4j public class MPPageUtil { public static Page convertTo(Page source, Page target, Class targetRecordClass) { + return MPPageUtil.convertTo(source, target, targetRecordClass, null); + } + + public static Page convertTo(Page source, Page target, Class targetRecordClass, BiFunction biFunction) { BeanUtils.copyProperties(source, target); List records = new ArrayList<>(); target.setRecords(records); @@ -21,6 +26,9 @@ public class MPPageUtil { for (T t : source.getRecords()) { U u = targetRecordClass.getDeclaredConstructor().newInstance(); BeanUtils.copyProperties(t, u); + if (null != biFunction) { + biFunction.apply(t, u); + } records.add(u); } } catch (NoSuchMethodException e1) { diff --git a/adi-common/src/main/java/com/moyz/adi/common/vo/AnswerMeta.java b/adi-common/src/main/java/com/moyz/adi/common/vo/AnswerMeta.java new file mode 100644 index 0000000..a94a7e8 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/vo/AnswerMeta.java @@ -0,0 +1,11 @@ +package com.moyz.adi.common.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class AnswerMeta { + private Integer tokens; + private String uuid; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/vo/ChatMeta.java b/adi-common/src/main/java/com/moyz/adi/common/vo/ChatMeta.java new file mode 100644 index 0000000..cb2118c --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/vo/ChatMeta.java @@ -0,0 +1,11 @@ +package com.moyz.adi.common.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class ChatMeta { + private QuestionMeta question; + private AnswerMeta answer; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/vo/CostStat.java b/adi-common/src/main/java/com/moyz/adi/common/vo/CostStat.java new file mode 100644 index 0000000..f395d91 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/vo/CostStat.java @@ -0,0 +1,14 @@ +package com.moyz.adi.common.vo; + +import lombok.Data; + +@Data +public class CostStat { + private int day; + private int textRequestTimesByDay; + private int textTokenCostByDay; + private int imageGeneratedNumberByDay; + private int textTokenCostByMonth; + private int textRequestTimesByMonth; + private int imageGeneratedNumberByMonth; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/vo/QuestionMeta.java b/adi-common/src/main/java/com/moyz/adi/common/vo/QuestionMeta.java new file mode 100644 index 0000000..6d17070 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/vo/QuestionMeta.java @@ -0,0 +1,11 @@ +package com.moyz.adi.common.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class QuestionMeta { + private Integer tokens; + private String uuid; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/vo/RequestRateLimit.java b/adi-common/src/main/java/com/moyz/adi/common/vo/RequestRateLimit.java new file mode 100644 index 0000000..b0ab87d --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/vo/RequestRateLimit.java @@ -0,0 +1,13 @@ +package com.moyz.adi.common.vo; + +import lombok.Data; + +@Data +public class RequestRateLimit { + private int times; + private int minutes; + private int type; + + public static final int TYPE_TEXT = 1; + public static final int TYPE_IMAGE = 2; +} diff --git a/adi-common/src/main/java/com/moyz/adi/common/vo/SseAskParams.java b/adi-common/src/main/java/com/moyz/adi/common/vo/SseAskParams.java new file mode 100644 index 0000000..2ae33f9 --- /dev/null +++ b/adi-common/src/main/java/com/moyz/adi/common/vo/SseAskParams.java @@ -0,0 +1,25 @@ +package com.moyz.adi.common.vo; + +import com.moyz.adi.common.entity.User; +import com.moyz.adi.common.util.TriConsumer; +import dev.langchain4j.memory.ChatMemory; +import lombok.Data; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + + +@Data +public class SseAskParams { + + private User user; + + private String regenerateQuestionUuid; + + private String systemMessage; + + private ChatMemory chatMemory; + + private String userMessage; + + private SseEmitter sseEmitter; + +} diff --git a/adi-common/src/main/resources/mapper/KnowledgeBaseEmbedding.xml b/adi-common/src/main/resources/mapper/KnowledgeBaseEmbedding.xml new file mode 100644 index 0000000..03ed9f5 --- /dev/null +++ b/adi-common/src/main/resources/mapper/KnowledgeBaseEmbedding.xml @@ -0,0 +1,12 @@ + + + + + + + + delete from adi_knowledge_base_embedding where metadata->>'kb_item_uuid' = #{kbItemUuid} + + diff --git a/adi-common/src/main/resources/mapper/KnowledgeBaseMapper.xml b/adi-common/src/main/resources/mapper/KnowledgeBaseMapper.xml new file mode 100644 index 0000000..f7bcc4a --- /dev/null +++ b/adi-common/src/main/resources/mapper/KnowledgeBaseMapper.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/adi-common/src/main/resources/mapper/KnowledgeBaseQaRecord.xml b/adi-common/src/main/resources/mapper/KnowledgeBaseQaRecord.xml new file mode 100644 index 0000000..236f4f6 --- /dev/null +++ b/adi-common/src/main/resources/mapper/KnowledgeBaseQaRecord.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/create.sql b/docs/create.sql index ce10e15..2d41254 100644 --- a/docs/create.sql +++ b/docs/create.sql @@ -1,186 +1,528 @@ -CREATE TABLE `adi_ai_model` -( - `id` bigint NOT NULL AUTO_INCREMENT, - `name` varchar(45) NOT NULL DEFAULT '', - `remark` varchar(1000) DEFAULT NULL, - `model_status` tinyint NOT NULL DEFAULT '1' COMMENT '1:正常使用,2:不可用', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (`id`) -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 - COLLATE = utf8mb4_general_ci COMMENT ='ai模型'; +-- 需要先安装pgvector这个扩展(https://github.com/pgvector/pgvector) +-- CREATE EXTENSION vector; -CREATE TABLE `adi_sys_config` -( - `id` bigint NOT NULL AUTO_INCREMENT, - `name` varchar(100) NOT NULL DEFAULT '', - `value` varchar(100) NOT NULL DEFAULT '', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `is_delete` tinyint NOT NULL DEFAULT '0', - PRIMARY KEY (`id`) -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 - COLLATE = utf8mb4_general_ci COMMENT ='系统配置表'; +SET client_encoding = 'UTF8'; +CREATE SCHEMA public; -INSERT INTO `adi_sys_config` (`name`, `value`) +CREATE TABLE public.adi_ai_image +( + id bigserial primary key, + user_id bigint DEFAULT '0'::bigint NOT NULL, + uuid character varying(32) DEFAULT ''::character varying NOT NULL, + prompt character varying(1024) DEFAULT ''::character varying NOT NULL, + generate_size character varying(20) DEFAULT ''::character varying NOT NULL, + generate_number integer DEFAULT 1 NOT NULL, + original_image character varying(1000) DEFAULT ''::character varying NOT NULL, + mask_image character varying(1000) DEFAULT ''::character varying NOT NULL, + resp_images_path character varying(2048) DEFAULT ''::character varying NOT NULL, + generated_images character varying(2048) DEFAULT ''::character varying NOT NULL, + interacting_method smallint DEFAULT '1'::smallint NOT NULL, + process_status smallint DEFAULT '1'::smallint NOT NULL, + create_time timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + update_time timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + is_deleted boolean DEFAULT false NOT NULL, + CONSTRAINT adi_ai_image_generate_number_check CHECK (((generate_number >= 1) AND (generate_number <= 10))), + CONSTRAINT adi_ai_image_interacting_method_check CHECK ((interacting_method = ANY (ARRAY [1, 2, 3]))), + CONSTRAINT adi_ai_image_process_status_check CHECK ((process_status = ANY (ARRAY [1, 2, 3]))), + CONSTRAINT adi_ai_image_user_id_check CHECK ((user_id >= 0)) +); +ALTER TABLE ONLY public.adi_ai_image + ADD CONSTRAINT udx_uuid UNIQUE (uuid); +COMMENT ON TABLE public.adi_ai_image IS 'Images generated by ai'; +COMMENT ON COLUMN public.adi_ai_image.user_id IS 'The user who generated the image'; +COMMENT ON COLUMN public.adi_ai_image.uuid IS 'The uuid of the request of generated images'; +COMMENT ON COLUMN public.adi_ai_image.prompt IS 'The prompt for generating images'; +COMMENT ON COLUMN public.adi_ai_image.generate_size IS 'The size of the generated images. Must be one of "256x256", "512x512", or "1024x1024"'; +COMMENT ON COLUMN public.adi_ai_image.generate_number IS 'The number of images to generate. Must be between 1 and 10. Defaults to 1.'; +COMMENT ON COLUMN public.adi_ai_image.original_image IS 'The path of the original image (local path or http path), interacting_method must be 2/3'; +COMMENT ON COLUMN public.adi_ai_image.mask_image IS 'The path of the mask image (local path or http path), interacting_method must be 2'; +COMMENT ON COLUMN public.adi_ai_image.resp_images_path IS 'The url of the generated images which from openai response, separated by commas'; + +COMMENT ON COLUMN public.adi_ai_image.generated_images IS 'The path of the generated images, separated by commas'; + +COMMENT ON COLUMN public.adi_ai_image.interacting_method IS '1: Creating images from scratch based on a text prompt; 2: Creating edits of an existing image based on a new text prompt; 3: Creating variations of an existing image'; +COMMENT ON COLUMN public.adi_ai_image.process_status IS 'Generate image status, 1: doing, 2: fail, 3: success'; +COMMENT ON COLUMN public.adi_ai_image.create_time IS 'Timestamp of record creation'; +COMMENT ON COLUMN public.adi_ai_image.update_time IS 'Timestamp of record last update, automatically updated on each update'; +COMMENT ON COLUMN public.adi_ai_image.is_deleted IS 'Flag indicating whether the record is deleted (0: not deleted, 1: deleted)'; + + +CREATE TABLE public.adi_ai_model +( + id bigserial primary key, + name character varying(45) DEFAULT ''::character varying NOT NULL, + remark character varying(1000), + model_status smallint DEFAULT '1'::smallint NOT NULL, + create_time timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + update_time timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + is_deleted boolean DEFAULT false NOT NULL, + CONSTRAINT adi_ai_model_model_status_check CHECK ((model_status = ANY (ARRAY [1, 2]))) +); + +COMMENT ON TABLE public.adi_ai_model IS 'ai模型'; + +COMMENT ON COLUMN public.adi_ai_model.name IS 'The name of the AI model'; +COMMENT ON COLUMN public.adi_ai_model.remark IS 'Additional remarks about the AI model'; +COMMENT ON COLUMN public.adi_ai_model.model_status IS '1: Normal usage, 2: Not available'; + +COMMENT ON COLUMN public.adi_ai_model.create_time IS 'Timestamp of record creation'; + +COMMENT ON COLUMN public.adi_ai_model.update_time IS 'Timestamp of record last update, automatically updated on each update'; + +CREATE TABLE public.adi_conversation +( + id bigserial primary key, + user_id bigint DEFAULT '0'::bigint NOT NULL, + uuid character varying(32) DEFAULT ''::character varying NOT NULL, + title character varying(45) DEFAULT ''::character varying NOT NULL, + openai_conversation_id character varying(32) DEFAULT ''::character varying NOT NULL, + tokens integer DEFAULT 0 NOT NULL, + ai_system_message character varying(1000) DEFAULT ''::character varying NOT NULL, + ai_model character varying(45) DEFAULT ''::character varying NOT NULL, + understand_context_enable boolean DEFAULT false NOT NULL, + create_time timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + update_time timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + is_deleted boolean DEFAULT false NOT NULL +); + +COMMENT ON TABLE public.adi_conversation IS '对话表'; + +COMMENT ON COLUMN public.adi_conversation.user_id IS '用户id'; + +COMMENT ON COLUMN public.adi_conversation.ai_model IS '模型名称'; + +COMMENT ON COLUMN public.adi_conversation.title IS '对话标题'; + +CREATE TABLE public.adi_conversation_message +( + id bigserial primary key, + parent_message_id bigint DEFAULT '0'::bigint NOT NULL, + conversation_id bigint DEFAULT '0'::bigint NOT NULL, + conversation_uuid character varying(32) DEFAULT ''::character varying NOT NULL, + remark text NOT NULL, + uuid character varying(32) DEFAULT ''::character varying NOT NULL, + message_role integer DEFAULT 1 NOT NULL, + tokens integer DEFAULT 0 NOT NULL, + openai_message_id character varying(32) DEFAULT ''::character varying NOT NULL, + user_id bigint DEFAULT '0'::bigint NOT NULL, + secret_key_type integer DEFAULT 1 NOT NULL, + understand_context_msg_pair_num integer DEFAULT 0 NOT NULL, + create_time timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + update_time timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + is_deleted boolean DEFAULT false NOT NULL +); +COMMENT ON TABLE public.adi_conversation_message IS '对话消息表'; + +COMMENT ON COLUMN public.adi_conversation_message.parent_message_id IS '父级消息id'; + +COMMENT ON COLUMN public.adi_conversation_message.conversation_id IS '对话id'; + +COMMENT ON COLUMN public.adi_conversation_message.conversation_uuid IS 'conversation''s uuid'; + +COMMENT ON COLUMN public.adi_conversation_message.remark IS 'ai回复的消息'; + +COMMENT ON COLUMN public.adi_conversation_message.uuid IS '唯一标识消息的UUID'; + +COMMENT ON COLUMN public.adi_conversation_message.message_role IS '产生该消息的角色:1: 用户, 2: 系统, 3: 助手'; + +COMMENT ON COLUMN public.adi_conversation_message.tokens IS '消耗的token数量'; + +COMMENT ON COLUMN public.adi_conversation_message.openai_message_id IS 'OpenAI生成的消息ID'; + +COMMENT ON COLUMN public.adi_conversation_message.user_id IS '用户ID'; + +COMMENT ON COLUMN public.adi_conversation_message.secret_key_type IS '加密密钥类型'; + +COMMENT ON COLUMN public.adi_conversation_message.understand_context_msg_pair_num IS '上下文消息对数量'; + +CREATE TABLE public.adi_file +( + id bigserial primary key, + name character varying(36) DEFAULT ''::character varying NOT NULL, + uuid character varying(32) DEFAULT ''::character varying NOT NULL, + ext character varying(36) DEFAULT ''::character varying NOT NULL, + user_id bigint DEFAULT '0'::bigint NOT NULL, + path character varying(250) DEFAULT ''::character varying NOT NULL, + ref_count integer DEFAULT 0 NOT NULL, + create_time timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + update_time timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + is_deleted boolean DEFAULT false NOT NULL, + md5 character varying(128) DEFAULT ''::character varying NOT NULL +); + +COMMENT ON TABLE public.adi_file IS '文件'; + +COMMENT ON COLUMN public.adi_file.name IS 'File name'; + +COMMENT ON COLUMN public.adi_file.uuid IS 'UUID of the file'; + +COMMENT ON COLUMN public.adi_file.ext IS 'File extension'; + +COMMENT ON COLUMN public.adi_file.user_id IS '0: System; Other: User'; + +COMMENT ON COLUMN public.adi_file.path IS 'File path'; + +COMMENT ON COLUMN public.adi_file.ref_count IS 'The number of references to this file'; + +COMMENT ON COLUMN public.adi_file.create_time IS 'Timestamp of record creation'; + +COMMENT ON COLUMN public.adi_file.update_time IS 'Timestamp of record last update, automatically updated on each update'; + +COMMENT ON COLUMN public.adi_file.is_deleted IS '0: Normal; 1: Deleted'; + +COMMENT ON COLUMN public.adi_file.md5 IS 'MD5 hash of the file'; + +CREATE TABLE public.adi_prompt +( + id bigserial primary key, + user_id bigint DEFAULT '0'::bigint NOT NULL, + act character varying(120) DEFAULT ''::character varying NOT NULL, + prompt text NOT NULL, + create_time timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + update_time timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + is_deleted boolean DEFAULT false NOT NULL +); + +COMMENT ON TABLE public.adi_prompt IS '提示词'; + +COMMENT ON COLUMN public.adi_prompt.user_id IS '所属用户(0: system)'; + +COMMENT ON COLUMN public.adi_prompt.act IS '提示词标题'; + +COMMENT ON COLUMN public.adi_prompt.prompt IS '提示词内容'; + +COMMENT ON COLUMN public.adi_prompt.create_time IS 'Timestamp of record creation'; + +COMMENT ON COLUMN public.adi_prompt.update_time IS 'Timestamp of record last update, automatically updated on each update'; + +COMMENT ON COLUMN public.adi_prompt.is_deleted IS '0:未删除;1:已删除'; + +CREATE TABLE public.adi_sys_config +( + id bigserial primary key, + name character varying(100) DEFAULT ''::character varying NOT NULL, + value character varying(100) DEFAULT ''::character varying NOT NULL, + create_time timestamp DEFAULT localtimestamp NOT NULL, + update_time timestamp DEFAULT localtimestamp NOT NULL, + is_deleted boolean DEFAULT false NOT NULL +); + +COMMENT ON TABLE public.adi_sys_config IS '系统配置表'; + +COMMENT ON COLUMN public.adi_sys_config.name IS '配置项名称'; + +COMMENT ON COLUMN public.adi_sys_config.value IS '配置项值'; + +COMMENT ON COLUMN public.adi_sys_config.create_time IS 'Timestamp of record creation'; + +COMMENT ON COLUMN public.adi_sys_config.update_time IS 'Timestamp of record last update, automatically updated on each update'; + +COMMENT ON COLUMN public.adi_sys_config.is_deleted IS '0:未删除;1:已删除'; + +CREATE TABLE public.adi_user +( + id bigserial primary key, + name character varying(45) DEFAULT ''::character varying NOT NULL, + password character varying(120) DEFAULT ''::character varying NOT NULL, + uuid character varying(32) DEFAULT ''::character varying NOT NULL, + email character varying(120) DEFAULT ''::character varying NOT NULL, + active_time timestamp, + user_status smallint DEFAULT '1'::smallint NOT NULL, + is_admin boolean DEFAULT false NOT NULL, + quota_by_token_daily integer DEFAULT 0 NOT NULL, + quota_by_token_monthly integer DEFAULT 0 NOT NULL, + quota_by_request_daily integer DEFAULT 0 NOT NULL, + quota_by_request_monthly integer DEFAULT 0 NOT NULL, + secret_key character varying(120) DEFAULT ''::character varying NOT NULL, + understand_context_enable smallint DEFAULT '0'::smallint NOT NULL, + understand_context_msg_pair_num integer DEFAULT 0 NOT NULL, + quota_by_image_daily integer DEFAULT 0 NOT NULL, + quota_by_image_monthly integer DEFAULT 0 NOT NULL, + create_time timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + update_time timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + is_deleted boolean DEFAULT false NOT NULL +); + +COMMENT ON TABLE public.adi_user IS '用户表'; + +COMMENT ON COLUMN public.adi_user.name IS '用户名'; + +COMMENT ON COLUMN public.adi_user.password IS '密码'; + +COMMENT ON COLUMN public.adi_user.uuid IS 'UUID of the user'; + +COMMENT ON COLUMN public.adi_user.email IS '用户邮箱'; + +COMMENT ON COLUMN public.adi_user.active_time IS '激活时间'; + +COMMENT ON COLUMN public.adi_user.create_time IS 'Timestamp of record creation'; + +COMMENT ON COLUMN public.adi_user.update_time IS 'Timestamp of record last update, automatically updated on each update'; + +COMMENT ON COLUMN public.adi_user.user_status IS '用户状态,1:待验证;2:正常;3:冻结'; + +COMMENT ON COLUMN public.adi_user.is_admin IS '是否管理员,0:否;1:是'; + +COMMENT ON COLUMN public.adi_user.is_deleted IS '0:未删除;1:已删除'; + +COMMENT ON COLUMN public.adi_user.quota_by_token_daily IS '每日token配额'; + +COMMENT ON COLUMN public.adi_user.quota_by_token_monthly IS '每月token配额'; + +COMMENT ON COLUMN public.adi_user.quota_by_request_daily IS '每日请求配额'; + +COMMENT ON COLUMN public.adi_user.quota_by_request_monthly IS '每月请求配额'; + +COMMENT ON COLUMN public.adi_user.secret_key IS '用户密钥'; + +COMMENT ON COLUMN public.adi_user.understand_context_enable IS '上下文理解开关'; + +COMMENT ON COLUMN public.adi_user.understand_context_msg_pair_num IS '上下文消息对数量'; + +COMMENT ON COLUMN public.adi_user.quota_by_image_daily IS '每日图片配额'; + +COMMENT ON COLUMN public.adi_user.quota_by_image_monthly IS '每月图片配额'; + +CREATE TABLE public.adi_user_day_cost +( + id bigserial primary key, + user_id bigint DEFAULT '0'::bigint NOT NULL, + day integer DEFAULT 0 NOT NULL, + requests integer DEFAULT 0 NOT NULL, + tokens integer DEFAULT 0 NOT NULL, + create_time timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + update_time timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + secret_key_type integer DEFAULT 0 NOT NULL, + images_number integer DEFAULT 0 NOT NULL, + is_deleted boolean DEFAULT false NOT NULL +); + +COMMENT ON TABLE public.adi_user_day_cost IS '用户每天消耗总量表'; + +COMMENT ON COLUMN public.adi_user_day_cost.user_id IS '用户ID'; + +COMMENT ON COLUMN public.adi_user_day_cost.day IS '日期,用7位整数表示,如20230901'; + +COMMENT ON COLUMN public.adi_user_day_cost.requests IS '请求数量'; + +COMMENT ON COLUMN public.adi_user_day_cost.tokens IS '消耗的token数量'; + +COMMENT ON COLUMN public.adi_user_day_cost.create_time IS 'Timestamp of record creation'; + +COMMENT ON COLUMN public.adi_user_day_cost.update_time IS 'Timestamp of record last update, automatically updated on each update'; + +COMMENT ON COLUMN public.adi_user_day_cost.secret_key_type IS '加密密钥类型'; + +COMMENT ON COLUMN public.adi_user_day_cost.images_number IS '图片数量'; + + +-- update_time trigger + +CREATE OR REPLACE FUNCTION update_modified_column() + RETURNS TRIGGER AS +$$ +BEGIN + NEW.update_time = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +CREATE TRIGGER trigger_ai_image_update_time + BEFORE UPDATE + ON adi_ai_image + FOR EACH ROW +EXECUTE PROCEDURE update_modified_column(); + +CREATE TRIGGER trigger_ai_model_update_time + BEFORE UPDATE + ON adi_ai_model + FOR EACH ROW +EXECUTE PROCEDURE update_modified_column(); + +CREATE TRIGGER trigger_conv_update_time + BEFORE UPDATE + ON adi_conversation + FOR EACH ROW +EXECUTE PROCEDURE update_modified_column(); + +CREATE TRIGGER trigger_conv_message_update_time + BEFORE UPDATE + ON adi_conversation_message + FOR EACH ROW +EXECUTE PROCEDURE update_modified_column(); + +CREATE TRIGGER trigger_file_update_time + BEFORE UPDATE + ON adi_file + FOR EACH ROW +EXECUTE PROCEDURE update_modified_column(); + +CREATE TRIGGER trigger_prompt_update_time + BEFORE UPDATE + ON adi_prompt + FOR EACH ROW +EXECUTE PROCEDURE update_modified_column(); + +CREATE TRIGGER trigger_sys_config_update_time + BEFORE UPDATE + ON adi_sys_config + FOR EACH ROW +EXECUTE PROCEDURE update_modified_column(); + +CREATE TRIGGER trigger_user_update_time + BEFORE UPDATE + ON adi_user + FOR EACH ROW +EXECUTE PROCEDURE update_modified_column(); + +CREATE TRIGGER trigger_user_day_cost_update_time + BEFORE UPDATE + ON adi_user_day_cost + FOR EACH ROW +EXECUTE PROCEDURE update_modified_column(); + +INSERT INTO adi_sys_config (name, value) VALUES ('secret_key', ''); -INSERT INTO `adi_sys_config` (`name`, `value`) +INSERT INTO adi_sys_config (name, value) VALUES ('request_text_rate_limit', '{"times":24,"minutes":3}'); -INSERT INTO `adi_sys_config` (`name`, `value`) +INSERT INTO adi_sys_config (name, value) VALUES ('request_image_rate_limit', '{"times":6,"minutes":3}'); -INSERT INTO `adi_sys_config` (`name`, `value`) +INSERT INTO adi_sys_config (name, value) VALUES ('conversation_max_num', '50'); -INSERT INTO `adi_sys_config` (`name`, `value`) +INSERT INTO adi_sys_config (name, value) VALUES ('quota_by_token_daily', '10000'); -INSERT INTO `adi_sys_config` (`name`, `value`) +INSERT INTO adi_sys_config (name, value) VALUES ('quota_by_token_monthly', '200000'); -INSERT INTO `adi_sys_config` (`name`, `value`) +INSERT INTO adi_sys_config (name, value) VALUES ('quota_by_request_daily', '150'); -INSERT INTO `adi_sys_config` (`name`, `value`) +INSERT INTO adi_sys_config (name, value) VALUES ('quota_by_request_monthly', '3000'); -INSERT INTO `adi_sys_config` (`name`, `value`) +INSERT INTO adi_sys_config (name, value) VALUES ('quota_by_image_daily', '30'); -INSERT INTO `adi_sys_config` (`name`, `value`) +INSERT INTO adi_sys_config (name, value) VALUES ('quota_by_image_monthly', '300'); +INSERT INTO adi_sys_config (name, value) +VALUES ('quota_by_qa_ask_daily', '50'); +INSERT INTO adi_sys_config (name, value) +VALUES ('quota_by_qa_item_monthly', '100'); -CREATE TABLE `adi_conversation` +create table adi_knowledge_base ( - `id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL DEFAULT '0' COMMENT '用户id', - `title` varchar(45) NOT NULL DEFAULT '' COMMENT '对话标题', - `uuid` varchar(32) NOT NULL DEFAULT '', - `understand_context_enable` tinyint NOT NULL default '0' COMMENT '是否开启上下文理解', - `ai_model` varchar(45) NOT NULL DEFAULT '' COMMENT 'ai model', - `ai_system_message` varchar(1024) NOT NULL DEFAULT '' COMMENT 'set the system message to ai, ig: you are a lawyer', - `tokens` int NOT NULL DEFAULT '0' COMMENT '消耗token数量', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `is_delete` tinyint NOT NULL DEFAULT '0', - PRIMARY KEY (`id`) -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 - COLLATE = utf8mb4_general_ci COMMENT ='对话表'; + id bigserial primary key, + uuid varchar(32) default ''::character varying not null, + title varchar(250) default ''::character varying not null, + remark text default ''::character varying not null, + is_public boolean default false not null, + owner_id bigint default 0 not null, + owner_name varchar(45) default ''::character varying not null, + create_time timestamp default CURRENT_TIMESTAMP not null, + update_time timestamp default CURRENT_TIMESTAMP not null, + is_deleted boolean default false not null +); -CREATE TABLE `adi_conversation_message` +comment on table adi_knowledge_base is '知识库'; + +comment on column adi_knowledge_base.title is '知识库名称'; + +comment on column adi_knowledge_base.remark is '知识库描述'; + +comment on column adi_knowledge_base.is_public is '是否公开'; + +comment on column adi_knowledge_base.owner_id is '所属人id'; + +comment on column adi_knowledge_base.owner_name is '所属人名称'; + +comment on column adi_knowledge_base.create_time is '创建时间'; + +comment on column adi_knowledge_base.update_time is '更新时间'; + +comment on column adi_knowledge_base.is_deleted is '0:未删除;1:已删除'; + +create trigger trigger_kb_update_time + before update + on adi_knowledge_base + for each row +execute procedure update_modified_column(); + +create table adi_knowledge_base_item ( - `id` bigint NOT NULL AUTO_INCREMENT, - `parent_message_id` bigint NOT NULL DEFAULT '0' COMMENT '父级消息id', - `conversation_id` bigint NOT NULL DEFAULT '0', - `conversation_uuid` varchar(32) NOT NULL DEFAULT '', - `user_id` bigint NOT NULL DEFAULT '0' COMMENT 'User id', - `content` text NOT NULL COMMENT '对话的消息', - `uuid` varchar(32) NOT NULL DEFAULT '', - `message_role` varchar(25) NOT NULL DEFAULT '' COMMENT '产生该消息的角色:1:user,2:system,3:assistant', - `tokens` int NOT NULL DEFAULT '0' COMMENT '消耗的token数量', - `secret_key_type` int NOT NULL DEFAULT '1' COMMENT '1:System secret key,2:User secret key', - `understand_context_msg_pair_num` int NOT NULL DEFAULT '0' COMMENT 'If context understanding enable, context_pair_msg_num > 0', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `is_delete` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除,0:未删除,1:已删除', - PRIMARY KEY (`id`), - UNIQUE KEY `udx_uuid` (`uuid`) -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 - COLLATE = utf8mb4_general_ci COMMENT ='对话信息表'; + id bigserial primary key, + uuid varchar(32) default ''::character varying not null, + kb_id bigint DEFAULT '0'::bigint NOT NULL, + kb_uuid varchar(32) default ''::character varying not null, + source_file_id bigint DEFAULT '0'::bigint NOT NULL, + title varchar(250) default ''::character varying not null, + brief varchar(250) default ''::character varying not null, + remark text default ''::character varying not null, + is_embedded boolean default false not null, + create_time timestamp default CURRENT_TIMESTAMP not null, + update_time timestamp default CURRENT_TIMESTAMP not null, + is_deleted boolean default false not null +); -CREATE TABLE `adi_ai_image` +comment on table adi_knowledge_base_item is '知识库-条目'; + +comment on column adi_knowledge_base_item.kb_id is '所属知识库id'; + +comment on column adi_knowledge_base_item.source_file_id is '来源文件id'; + +comment on column adi_knowledge_base_item.title is '条目标题'; + +comment on column adi_knowledge_base_item.brief is '条目内容摘要'; + +comment on column adi_knowledge_base_item.remark is '条目内容'; + +comment on column adi_knowledge_base_item.is_embedded is '是否已向量化,0:否,1:是'; + +comment on column adi_knowledge_base_item.create_time is '创建时间'; + +comment on column adi_knowledge_base_item.update_time is '更新时间'; + +comment on column adi_knowledge_base_item.is_deleted is '0:未删除;1:已删除'; + +create trigger trigger_kb_item_update_time + before update + on adi_knowledge_base_item + for each row +execute procedure update_modified_column(); + +create table adi_knowledge_base_qa_record ( - `id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL DEFAULT '0' COMMENT 'The user who generated the image', - `uuid` varchar(32) NOT NULL DEFAULT '' COMMENT 'The uuid of the request of generated images', - `prompt` varchar(1024) NOT NULL DEFAULT '' COMMENT 'The prompt for generating images', - `generate_size` varchar(20) NOT NULL DEFAULT '' COMMENT 'The size of the generated images. Must be one of "256x256", "512x512", or "1024x1024"', - `generate_number` int NOT NULL DEFAULT '1' COMMENT 'The number of images to generate. Must be between 1 and 10. Defaults to 1.', - `original_image` varchar(32) NOT NULL DEFAULT '' COMMENT 'The original image uuid,interacting_method must be 2/3', - `mask_image` varchar(32) NOT NULL DEFAULT '' COMMENT 'The mask image uuid,interacting_method must be 2', - `resp_images_path` varchar(2048) NOT NULL DEFAULT '' COMMENT 'The url of the generated images which from openai response,separated by commas', - `generated_images` varchar(512) NOT NULL DEFAULT '' COMMENT 'The uuid of the generated images,separated by commas', - `interacting_method` smallint NOT NULL DEFAULT '1' COMMENT '1:Creating images from scratch based on a text prompt;2:Creating edits of an existing image based on a new text prompt;3:Creating variations of an existing image', - `process_status` smallint NOT NULL DEFAULT '1' COMMENT 'Process status,1:processing,2:fail,3:success', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `is_delete` tinyint NOT NULL DEFAULT '0', - PRIMARY KEY (`id`), - UNIQUE KEY `udx_uuid` (`uuid`) -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 - COLLATE = utf8mb4_general_ci COMMENT ='Images generated by ai'; + id bigserial primary key, + uuid varchar(32) default ''::character varying not null, + kb_id bigint DEFAULT '0'::bigint NOT NULL, + kb_uuid varchar(32) default ''::character varying not null, + question varchar(1000) default ''::character varying not null, + answer text default ''::character varying not null, + source_file_ids varchar(500) default ''::character varying not null, + user_id bigint default '0' NOT NULL, + create_time timestamp default CURRENT_TIMESTAMP not null, + update_time timestamp default CURRENT_TIMESTAMP not null, + is_deleted boolean default false not null +); -CREATE TABLE `adi_user` -( - `id` bigint NOT NULL AUTO_INCREMENT, - `name` varchar(45) NOT NULL DEFAULT '', - `password` varchar(120) NOT NULL DEFAULT '', - `uuid` varchar(32) NOT NULL DEFAULT '', - `email` varchar(120) NOT NULL DEFAULT '', - `active_time` datetime NULL COMMENT '激活时间', - `secret_key` varchar(120) NOT NULL default '' COMMENT 'Custom openai secret key', - `understand_context_msg_pair_num` int NOT NULL default '0' COMMENT '上下文理解中需要携带的消息对数量(提示词及回复)', - `quota_by_token_daily` int NOT NULL DEFAULT '0' COMMENT 'The quota of token daily', - `quota_by_token_monthly` int NOT NULL DEFAULT '0' COMMENT 'The quota of token monthly', - `quota_by_request_daily` int NOT NULL DEFAULT '0' COMMENT 'The quota of http request daily', - `quota_by_request_monthly` int NOT NULL DEFAULT '0' COMMENT 'The quota of http request monthly', - `quota_by_image_daily` int NOT NULL DEFAULT '0' COMMENT 'The quota of generate images daily', - `quota_by_image_monthly` int NOT NULL DEFAULT '0' COMMENT 'The quota of generate images monthly', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `user_status` smallint NOT NULL DEFAULT '1' COMMENT '用户状态,1:待验证;2:正常;3:冻结', - `is_delete` tinyint NOT NULL DEFAULT '0' COMMENT '0:未删除;1:已删除', - PRIMARY KEY (`id`), - UNIQUE KEY `udx_uuid` (`uuid`) -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 - COLLATE = utf8mb4_general_ci COMMENT ='用户表'; +comment on table adi_knowledge_base_qa_record is '知识库-提问记录'; -CREATE TABLE `adi_user_day_cost` -( - `id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL DEFAULT '0', - `day` int NOT NULL DEFAULT '0' COMMENT '日期,用7位整数表示,如20230901', - `requests` int NOT NULL DEFAULT '0' COMMENT 'The number of http request', - `tokens` int NOT NULL DEFAULT '0' COMMENT 'The cost of the tokens', - `images_number` int NOT NULL DEFAULT '0' COMMENT 'The number of images', - `secret_key_type` int NOT NULL DEFAULT '1' COMMENT '1:System secret key,2:Custom secret key', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (`id`) -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 - COLLATE = utf8mb4_general_ci COMMENT ='用户每天消耗总量表'; +comment on column adi_knowledge_base_qa_record.kb_id is '所属知识库id'; +comment on column adi_knowledge_base_qa_record.kb_uuid is '所属知识库uuid'; -CREATE TABLE `adi_prompt` -( - `id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL DEFAULT '0' COMMENT '0:System,other:User', - `act` varchar(120) NOT NULL DEFAULT '' COMMENT 'Short prompt for search/autocomplete', - `prompt` varchar(1024) NOT NULL COMMENT 'Prompt content', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `is_delete` tinyint NOT NULL DEFAULT '0' COMMENT '0:Normal;1:Deleted', - PRIMARY KEY (`id`), - KEY `idx_title` (`act`) -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 - COLLATE = utf8mb4_general_ci COMMENT ='提示词'; +comment on column adi_knowledge_base_qa_record.question is '问题'; -CREATE TABLE `adi_file` -( - `id` bigint NOT NULL AUTO_INCREMENT, - `name` varchar(32) NOT NULL DEFAULT '', - `uuid` varchar(32) NOT NULL DEFAULT '', - `md5` varchar(128) NOT NULL DEFAULT '', - `ext` varchar(32) NOT NULL DEFAULT '', - `user_id` bigint NOT NULL DEFAULT '0' COMMENT '0:System,other:User', - `path` varchar(250) NOT NULL DEFAULT '', - `ref_count` int NOT NULL DEFAULT '0' COMMENT 'The number of references to this file', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `is_delete` tinyint NOT NULL DEFAULT '0' COMMENT '0:Normal;1:Deleted', - PRIMARY KEY (`id`), - KEY `idx_uuid` (`uuid`) -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 - COLLATE = utf8mb4_general_ci COMMENT ='提示词'; \ No newline at end of file +comment on column adi_knowledge_base_qa_record.answer is '答案'; + +comment on column adi_knowledge_base_qa_record.source_file_ids is '来源文档id,以逗号隔开'; + +comment on column adi_knowledge_base_qa_record.user_id is '提问用户id'; + +comment on column adi_knowledge_base_qa_record.create_time is '创建时间'; + +comment on column adi_knowledge_base_qa_record.update_time is '更新时间'; + +comment on column adi_knowledge_base_qa_record.is_deleted is '0:未删除;1:已删除'; + +create trigger trigger_kb_qa_record_update_time + before update + on adi_knowledge_base_qa_record + for each row +execute procedure update_modified_column(); diff --git a/pom.xml b/pom.xml index 707252f..2005208 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,7 @@ 17 17 UTF-8 + 0.25.0 @@ -68,9 +69,8 @@ 31.1-jre - com.mysql - mysql-connector-j - 8.0.32 + org.postgresql + postgresql com.theokanning.openai-gpt3-java @@ -130,6 +130,36 @@ velocity-engine-core 2.3 + + dev.langchain4j + langchain4j + ${langchain4j.version} + + + dev.langchain4j + langchain4j-open-ai-spring-boot-starter + ${langchain4j.version} + + + dev.langchain4j + langchain4j-pgvector + ${langchain4j.version} + + + dev.langchain4j + langchain4j-embeddings-all-minilm-l6-v2 + ${langchain4j.version} + + + dev.langchain4j + langchain4j-document-parser-apache-pdfbox + ${langchain4j.version} + + + dev.langchain4j + langchain4j-document-parser-apache-poi + ${langchain4j.version} + org.springframework.boot spring-boot-starter-test