diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 0b141bd..81114d6 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-quartz") implementation("dev.langchain4j:langchain4j:1.0.0") implementation("dev.langchain4j:langchain4j-open-ai:1.0.0") + implementation("dev.langchain4j:langchain4j-pgvector:1.0.1-beta6") implementation("dev.langchain4j:langchain4j-community-zhipu-ai:1.0.1-beta6") implementation("io.projectreactor:reactor-core:3.7.6") testImplementation("org.testcontainers:junit-jupiter:$testcontainersVersion") diff --git a/backend/src/main/java/com/zl/mjga/config/ai/ChatModelConfig.java b/backend/src/main/java/com/zl/mjga/config/ai/ChatModelInitializer.java similarity index 69% rename from backend/src/main/java/com/zl/mjga/config/ai/ChatModelConfig.java rename to backend/src/main/java/com/zl/mjga/config/ai/ChatModelInitializer.java index 936bf74..49d109d 100644 --- a/backend/src/main/java/com/zl/mjga/config/ai/ChatModelConfig.java +++ b/backend/src/main/java/com/zl/mjga/config/ai/ChatModelInitializer.java @@ -14,17 +14,17 @@ import org.springframework.context.annotation.DependsOn; @Configuration @RequiredArgsConstructor -public class ChatModelConfig { +public class ChatModelInitializer { private final LlmService llmService; private final PromptConfiguration promptConfiguration; @Bean @DependsOn("flywayInitializer") - public ZhipuAiStreamingChatModel zhipuChatModel(ZhiPuConfiguration zhiPuConfiguration) { + public ZhipuAiStreamingChatModel zhipuChatModel(ZhiPuChatModelConfig zhiPuChatModelConfig) { return ZhipuAiStreamingChatModel.builder() - .model(zhiPuConfiguration.getModelName()) - .apiKey(zhiPuConfiguration.getApiKey()) + .model(zhiPuChatModelConfig.getModelName()) + .apiKey(zhiPuChatModelConfig.getApiKey()) .logRequests(true) .logResponses(true) .build(); @@ -32,11 +32,12 @@ public class ChatModelConfig { @Bean @DependsOn("flywayInitializer") - public OpenAiStreamingChatModel deepSeekChatModel(DeepSeekConfiguration deepSeekConfiguration) { + public OpenAiStreamingChatModel deepSeekChatModel( + DeepSeekChatModelConfig deepSeekChatModelConfig) { return OpenAiStreamingChatModel.builder() - .baseUrl(deepSeekConfiguration.getBaseUrl()) - .apiKey(deepSeekConfiguration.getApiKey()) - .modelName(deepSeekConfiguration.getModelName()) + .baseUrl(deepSeekChatModelConfig.getBaseUrl()) + .apiKey(deepSeekChatModelConfig.getApiKey()) + .modelName(deepSeekChatModelConfig.getModelName()) .build(); } @@ -62,19 +63,19 @@ public class ChatModelConfig { @Bean @DependsOn("flywayInitializer") - public DeepSeekConfiguration deepSeekConfiguration() { - DeepSeekConfiguration deepSeekConfiguration = new DeepSeekConfiguration(); + public DeepSeekChatModelConfig deepSeekConfiguration() { + DeepSeekChatModelConfig deepSeekChatModelConfig = new DeepSeekChatModelConfig(); AiLlmConfig deepSeek = llmService.loadConfig(LlmCodeEnum.DEEP_SEEK); - deepSeekConfiguration.init(deepSeek); - return deepSeekConfiguration; + deepSeekChatModelConfig.init(deepSeek); + return deepSeekChatModelConfig; } @Bean @DependsOn("flywayInitializer") - public ZhiPuConfiguration zhiPuConfiguration() { - ZhiPuConfiguration zhiPuConfiguration = new ZhiPuConfiguration(); + public ZhiPuChatModelConfig zhiPuConfiguration() { + ZhiPuChatModelConfig zhiPuChatModelConfig = new ZhiPuChatModelConfig(); AiLlmConfig aiLlmConfig = llmService.loadConfig(LlmCodeEnum.ZHI_PU); - zhiPuConfiguration.init(aiLlmConfig); - return zhiPuConfiguration; + zhiPuChatModelConfig.init(aiLlmConfig); + return zhiPuChatModelConfig; } } diff --git a/backend/src/main/java/com/zl/mjga/config/ai/ZhiPuConfiguration.java b/backend/src/main/java/com/zl/mjga/config/ai/DeepSeekChatModelConfig.java similarity index 79% rename from backend/src/main/java/com/zl/mjga/config/ai/ZhiPuConfiguration.java rename to backend/src/main/java/com/zl/mjga/config/ai/DeepSeekChatModelConfig.java index f94a679..29c17f0 100644 --- a/backend/src/main/java/com/zl/mjga/config/ai/ZhiPuConfiguration.java +++ b/backend/src/main/java/com/zl/mjga/config/ai/DeepSeekChatModelConfig.java @@ -2,11 +2,9 @@ package com.zl.mjga.config.ai; import lombok.Data; import org.jooq.generated.mjga.tables.pojos.AiLlmConfig; -import org.springframework.stereotype.Component; @Data -@Component -public class ZhiPuConfiguration { +public class DeepSeekChatModelConfig { private String baseUrl; private String apiKey; diff --git a/backend/src/main/java/com/zl/mjga/config/ai/EmbeddingInitializer.java b/backend/src/main/java/com/zl/mjga/config/ai/EmbeddingInitializer.java new file mode 100644 index 0000000..bc81f98 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/config/ai/EmbeddingInitializer.java @@ -0,0 +1,58 @@ +package com.zl.mjga.config.ai; + +import com.zl.mjga.service.LlmService; +import dev.langchain4j.community.model.zhipu.ZhipuAiEmbeddingModel; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.store.embedding.EmbeddingStore; +import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import org.jooq.generated.mjga.enums.LlmCodeEnum; +import org.jooq.generated.mjga.tables.pojos.AiLlmConfig; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.core.env.Environment; + +@Configuration +@RequiredArgsConstructor +public class EmbeddingInitializer { + + @Resource private Environment env; + private final LlmService llmService; + + @Bean + @DependsOn("flywayInitializer") + public ZhiPuEmbeddingModelConfig zhiPuEmbeddingModelConfig() { + ZhiPuEmbeddingModelConfig zhiPuEmbeddingModelConfig = new ZhiPuEmbeddingModelConfig(); + AiLlmConfig aiLlmConfig = llmService.loadConfig(LlmCodeEnum.ZHI_PU_EMBEDDING); + zhiPuEmbeddingModelConfig.init(aiLlmConfig); + return zhiPuEmbeddingModelConfig; + } + + @Bean + @DependsOn("flywayInitializer") + public EmbeddingModel zhipuEmbeddingModel(ZhiPuEmbeddingModelConfig zhiPuEmbeddingModelConfig) { + return ZhipuAiEmbeddingModel.builder() + .apiKey(zhiPuEmbeddingModelConfig.getApiKey()) + .model(zhiPuEmbeddingModelConfig.getModelName()) + .dimensions(2048) + .build(); + } + + @Bean + public EmbeddingStore zhiPuEmbeddingStore(EmbeddingModel zhipuEmbeddingModel) { + String hostPort = env.getProperty("DATABASE_HOST_PORT"); + String host = hostPort.split(":")[0]; + return PgVectorEmbeddingStore.builder() + .host(host) + .port(env.getProperty("DATABASE_EXPOSE_PORT", Integer.class)) + .database(env.getProperty("DATABASE_DB")) + .user(env.getProperty("DATABASE_USER")) + .password(env.getProperty("DATABASE_PASSWORD")) + .table("mjga.zhipu_embedding_store") + .dimension(2048) + .build(); + } +} diff --git a/backend/src/main/java/com/zl/mjga/config/ai/DeepSeekConfiguration.java b/backend/src/main/java/com/zl/mjga/config/ai/ZhiPuChatModelConfig.java similarity index 90% rename from backend/src/main/java/com/zl/mjga/config/ai/DeepSeekConfiguration.java rename to backend/src/main/java/com/zl/mjga/config/ai/ZhiPuChatModelConfig.java index 9b23819..a08e812 100644 --- a/backend/src/main/java/com/zl/mjga/config/ai/DeepSeekConfiguration.java +++ b/backend/src/main/java/com/zl/mjga/config/ai/ZhiPuChatModelConfig.java @@ -4,7 +4,7 @@ import lombok.Data; import org.jooq.generated.mjga.tables.pojos.AiLlmConfig; @Data -public class DeepSeekConfiguration { +public class ZhiPuChatModelConfig { private String baseUrl; private String apiKey; diff --git a/backend/src/main/java/com/zl/mjga/config/ai/ZhiPuEmbeddingModelConfig.java b/backend/src/main/java/com/zl/mjga/config/ai/ZhiPuEmbeddingModelConfig.java new file mode 100644 index 0000000..bb41312 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/config/ai/ZhiPuEmbeddingModelConfig.java @@ -0,0 +1,20 @@ +package com.zl.mjga.config.ai; + +import lombok.Data; +import org.jooq.generated.mjga.tables.pojos.AiLlmConfig; + +@Data +public class ZhiPuEmbeddingModelConfig { + + private String baseUrl; + private String apiKey; + private String modelName; + private Boolean enable; + + public void init(AiLlmConfig config) { + this.baseUrl = config.getUrl(); + this.apiKey = config.getApiKey(); + this.modelName = config.getModelName(); + this.enable = config.getEnable(); + } +} diff --git a/backend/src/main/java/com/zl/mjga/controller/AiController.java b/backend/src/main/java/com/zl/mjga/controller/AiController.java index b77f900..b95d5fd 100644 --- a/backend/src/main/java/com/zl/mjga/controller/AiController.java +++ b/backend/src/main/java/com/zl/mjga/controller/AiController.java @@ -4,15 +4,20 @@ import com.zl.mjga.dto.PageRequestDto; import com.zl.mjga.dto.PageResponseDto; import com.zl.mjga.dto.ai.LlmQueryDto; import com.zl.mjga.dto.ai.LlmVm; +import com.zl.mjga.exception.BusinessException; import com.zl.mjga.service.AiChatService; +import com.zl.mjga.service.EmbeddingService; import com.zl.mjga.service.LlmService; import dev.langchain4j.service.TokenStream; import jakarta.validation.Valid; import java.security.Principal; import java.time.Duration; import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.jooq.generated.mjga.enums.LlmCodeEnum; +import org.jooq.generated.mjga.tables.pojos.AiLlmConfig; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; @@ -28,6 +33,7 @@ public class AiController { private final AiChatService aiChatService; private final LlmService llmService; + private final EmbeddingService embeddingService; @PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux chat(Principal principal, @RequestBody String userMessage) { @@ -57,4 +63,13 @@ public class AiController { @ModelAttribute PageRequestDto pageRequestDto, @ModelAttribute LlmQueryDto llmQueryDto) { return llmService.pageQueryLlm(pageRequestDto, llmQueryDto); } + + @PostMapping("/action/chat") + public Map actionChat(@RequestBody String message) { + AiLlmConfig aiLlmConfig = llmService.loadConfig(LlmCodeEnum.ZHI_PU); + if (!aiLlmConfig.getEnable()) { + throw new BusinessException("命令模型未启用,请开启后再试。"); + } + return embeddingService.searchAction(message); + } } diff --git a/backend/src/main/java/com/zl/mjga/dto/ai/LlmQueryDto.java b/backend/src/main/java/com/zl/mjga/dto/ai/LlmQueryDto.java index d9fec07..a462b0a 100644 --- a/backend/src/main/java/com/zl/mjga/dto/ai/LlmQueryDto.java +++ b/backend/src/main/java/com/zl/mjga/dto/ai/LlmQueryDto.java @@ -1,3 +1,3 @@ package com.zl.mjga.dto.ai; -public record LlmQueryDto(String name) {} +public record LlmQueryDto(String name, String type) {} diff --git a/backend/src/main/java/com/zl/mjga/dto/ai/LlmVm.java b/backend/src/main/java/com/zl/mjga/dto/ai/LlmVm.java index a188528..554fc07 100644 --- a/backend/src/main/java/com/zl/mjga/dto/ai/LlmVm.java +++ b/backend/src/main/java/com/zl/mjga/dto/ai/LlmVm.java @@ -12,6 +12,8 @@ public class LlmVm { @NotEmpty(message = "模型名称不能为空") private String modelName; + @NotEmpty(message = "模型类型不能为空") private String type; + @NotEmpty(message = "apikey 不能为空") private String apiKey; @NotEmpty(message = "url 不能为空") private String url; diff --git a/backend/src/main/java/com/zl/mjga/model/urp/Actions.java b/backend/src/main/java/com/zl/mjga/model/urp/Actions.java new file mode 100644 index 0000000..30ec51c --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/model/urp/Actions.java @@ -0,0 +1,14 @@ +package com.zl.mjga.model.urp; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum Actions { + CREATE_USER("CREATE_USER", "创建新用户"), + DELETE_USER("DELETE_USER", "删除用户"); + public static final String INDEX_KEY = "action"; + private final String code; + private final String content; +} diff --git a/backend/src/main/java/com/zl/mjga/repository/LlmRepository.java b/backend/src/main/java/com/zl/mjga/repository/LlmRepository.java index 0e6dfa1..b0b4d61 100644 --- a/backend/src/main/java/com/zl/mjga/repository/LlmRepository.java +++ b/backend/src/main/java/com/zl/mjga/repository/LlmRepository.java @@ -9,6 +9,7 @@ import org.apache.commons.lang3.StringUtils; import org.jooq.Configuration; import org.jooq.Record; import org.jooq.Result; +import org.jooq.generated.default_schema.enums.LlmTypeEnum; import org.jooq.generated.mjga.tables.daos.AiLlmConfigDao; import org.jooq.impl.DSL; import org.springframework.beans.factory.annotation.Autowired; @@ -25,12 +26,17 @@ public class LlmRepository extends AiLlmConfigDao { public Result pageFetchBy(PageRequestDto pageRequestDto, LlmQueryDto llmQueryDto) { return ctx() .select( - AI_LLM_CONFIG.asterisk(), DSL.count().over().as("total_llm").convertFrom(Long::valueOf)) + AI_LLM_CONFIG.asterisk(), + DSL.count().over().as("total_llm").convertFrom(Long::valueOf)) .from(AI_LLM_CONFIG) .where( StringUtils.isNotEmpty(llmQueryDto.name()) ? AI_LLM_CONFIG.NAME.eq(llmQueryDto.name()) : noCondition()) + .and( + StringUtils.isNotEmpty(llmQueryDto.type()) + ? AI_LLM_CONFIG.TYPE.eq(LlmTypeEnum.lookupLiteral(llmQueryDto.type())) + : noCondition()) .orderBy(pageRequestDto.getSortFields()) .limit(pageRequestDto.getSize()) .offset(pageRequestDto.getOffset()) diff --git a/backend/src/main/java/com/zl/mjga/service/AiChatService.java b/backend/src/main/java/com/zl/mjga/service/AiChatService.java index 9612281..a77586d 100644 --- a/backend/src/main/java/com/zl/mjga/service/AiChatService.java +++ b/backend/src/main/java/com/zl/mjga/service/AiChatService.java @@ -28,12 +28,13 @@ public class AiChatService { } public TokenStream chatPrecedenceLlmWith(String sessionIdentifier, String userMessage) { - Optional precedenceLlmBy = llmService.getPrecedenceLlmBy(true); + Optional precedenceLlmBy = llmService.getPrecedenceChatLlmBy(true); AiLlmConfig aiLlmConfig = precedenceLlmBy.orElseThrow(() -> new BusinessException("没有开启的大模型")); LlmCodeEnum code = aiLlmConfig.getCode(); return switch (code) { case ZHI_PU -> zhiPuChatAssistant.chat(sessionIdentifier, userMessage); case DEEP_SEEK -> deepSeekChatAssistant.chat(sessionIdentifier, userMessage); + default -> throw new BusinessException(String.format("无效的模型代码 %s", code)); }; } } diff --git a/backend/src/main/java/com/zl/mjga/service/EmbeddingService.java b/backend/src/main/java/com/zl/mjga/service/EmbeddingService.java new file mode 100644 index 0000000..d13e109 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/service/EmbeddingService.java @@ -0,0 +1,72 @@ +package com.zl.mjga.service; + +import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.metadataKey; + +import com.zl.mjga.config.ai.ZhiPuEmbeddingModelConfig; +import com.zl.mjga.model.urp.Actions; +import dev.langchain4j.data.document.Metadata; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.store.embedding.EmbeddingSearchRequest; +import dev.langchain4j.store.embedding.EmbeddingSearchResult; +import dev.langchain4j.store.embedding.EmbeddingStore; +import dev.langchain4j.store.embedding.filter.Filter; +import jakarta.annotation.PostConstruct; +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Service; + +@Configuration +@RequiredArgsConstructor +@Service +public class EmbeddingService { + + private final EmbeddingModel zhipuEmbeddingModel; + + private final EmbeddingStore zhiPuEmbeddingStore; + + private final ZhiPuEmbeddingModelConfig zhiPuEmbeddingModelConfig; + + public Map searchAction(String message) { + Map result = new HashMap<>(); + EmbeddingSearchRequest embeddingSearchRequest = + EmbeddingSearchRequest.builder() + .queryEmbedding(zhipuEmbeddingModel.embed(message).content()) + .build(); + EmbeddingSearchResult embeddingSearchResult = + zhiPuEmbeddingStore.search(embeddingSearchRequest); + if (!embeddingSearchResult.matches().isEmpty()) { + Metadata metadata = embeddingSearchResult.matches().getFirst().embedded().metadata(); + result.put(Actions.INDEX_KEY, metadata.getString(Actions.INDEX_KEY)); + } + return result; + } + + @PostConstruct + public void initActionIndex() { + if (!zhiPuEmbeddingModelConfig.getEnable()) { + return; + } + for (Actions action : Actions.values()) { + Embedding queryEmbedding = zhipuEmbeddingModel.embed(action.getContent()).content(); + Filter createUserFilter = metadataKey(Actions.INDEX_KEY).isEqualTo(action.getCode()); + EmbeddingSearchRequest embeddingSearchRequest = + EmbeddingSearchRequest.builder() + .queryEmbedding(queryEmbedding) + .filter(createUserFilter) + .build(); + EmbeddingSearchResult embeddingSearchResult = + zhiPuEmbeddingStore.search(embeddingSearchRequest); + if (embeddingSearchResult.matches().isEmpty()) { + TextSegment segment = + TextSegment.from( + action.getContent(), Metadata.metadata(Actions.INDEX_KEY, action.getCode())); + Embedding embedding = zhipuEmbeddingModel.embed(segment).content(); + zhiPuEmbeddingStore.add(embedding, segment); + } + } + } +} diff --git a/backend/src/main/java/com/zl/mjga/service/LlmService.java b/backend/src/main/java/com/zl/mjga/service/LlmService.java index 54d0415..3ab30d9 100644 --- a/backend/src/main/java/com/zl/mjga/service/LlmService.java +++ b/backend/src/main/java/com/zl/mjga/service/LlmService.java @@ -12,11 +12,14 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.jooq.Record; import org.jooq.Result; +import org.jooq.generated.default_schema.enums.LlmTypeEnum; import org.jooq.generated.mjga.enums.LlmCodeEnum; import org.jooq.generated.mjga.tables.pojos.AiLlmConfig; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; +import static org.jooq.generated.mjga.Tables.AI_LLM_CONFIG; + @Service @RequiredArgsConstructor @Slf4j @@ -28,9 +31,11 @@ public class LlmService { return llmRepository.fetchOneByCode(llmCodeEnum); } - public Optional getPrecedenceLlmBy(Boolean enable) { + public Optional getPrecedenceChatLlmBy(Boolean enable) { List aiLlmConfigs = llmRepository.fetchByEnable(enable); - return aiLlmConfigs.stream().max((o1, o2) -> o2.getPriority().compareTo(o1.getPriority())); + return aiLlmConfigs.stream() + .filter(aiLlmConfig -> LlmTypeEnum.CHAT.equals(aiLlmConfig.getType())) + .max((o1, o2) -> o2.getPriority().compareTo(o1.getPriority())); } public PageResponseDto> pageQueryLlm( @@ -39,7 +44,11 @@ public class LlmService { if (records.isEmpty()) { return PageResponseDto.empty(); } - List llmVms = records.into(LlmVm.class); + List llmVms = records.map((record) -> { + LlmVm into = record.into(LlmVm.class); + into.setType(record.get(AI_LLM_CONFIG.TYPE).getLiteral()); + return into; + }); Long totalLlm = records.get(0).getValue("total_llm", Long.class); return new PageResponseDto<>(totalLlm, llmVms); } diff --git a/backend/src/main/resources/db/migration/V1_0_0__init_table.sql b/backend/src/main/resources/db/migration/V1_0_0__init_table.sql index 7a25894..f214ef9 100644 --- a/backend/src/main/resources/db/migration/V1_0_0__init_table.sql +++ b/backend/src/main/resources/db/migration/V1_0_0__init_table.sql @@ -68,15 +68,21 @@ CREATE TABLE mjga.user_position_map ( CREATE TYPE mjga.llm_code_enum AS ENUM ( 'DEEP_SEEK', - 'ZHI_PU' + 'ZHI_PU', + 'ZHI_PU_EMBEDDING' ); +CREATE TYPE "llm_type_enum" AS ENUM ( + 'CHAT', + 'EMBEDDING' +); CREATE TABLE mjga.ai_llm_config ( id BIGSERIAL NOT NULL UNIQUE, name VARCHAR(255) NOT NULL UNIQUE, code mjga.llm_code_enum NOT NULL UNIQUE, model_name VARCHAR(255) NOT NULL, + type LLM_TYPE_ENUM NOT NULL, api_key VARCHAR(255) NOT NULL, url VARCHAR(255) NOT NULL, enable BOOLEAN NOT NULL DEFAULT true, diff --git a/backend/src/main/resources/db/migration/V1_0_1__insert_init_table.sql b/backend/src/main/resources/db/migration/V1_0_1__insert_init_table.sql index 05b4ee9..2531d91 100644 --- a/backend/src/main/resources/db/migration/V1_0_1__insert_init_table.sql +++ b/backend/src/main/resources/db/migration/V1_0_1__insert_init_table.sql @@ -33,7 +33,8 @@ VALUES (1, 1), (1, 9), (1, 10); -INSERT INTO mjga.ai_llm_config (name,code,model_name, api_key, url, enable, priority) +INSERT INTO mjga.ai_llm_config (name,code,model_name, type, api_key, url, enable, priority) VALUES - ('DeepSeek','DEEP_SEEK','deepseek-chat','your_api_key', 'https://api.deepseek.com', false, 0), - ('智谱清言','ZHI_PU','glm-4-flash', 'your_api_key', 'https://open.bigmodel.cn/', false, 1); \ No newline at end of file + ('DeepSeek','DEEP_SEEK','deepseek-chat','CHAT','your_api_key', 'https://api.deepseek.com', false, 0), + ('智谱清言','ZHI_PU','glm-4-flash','CHAT', 'your_api_key', 'https://open.bigmodel.cn/', false, 1), + ('智谱清言向量','ZHI_PU_EMBEDDING','Embeddings-3','EMBEDDING', 'your_api_key', 'https://open.bigmodel.cn/', false, 0); \ No newline at end of file diff --git a/backend/src/test/resources/db/migration/test/V1_0_0__init_table.sql b/backend/src/test/resources/db/migration/test/V1_0_0__init_table.sql index 00dc253..a61d3be 100644 --- a/backend/src/test/resources/db/migration/test/V1_0_0__init_table.sql +++ b/backend/src/test/resources/db/migration/test/V1_0_0__init_table.sql @@ -66,9 +66,23 @@ CREATE TABLE mjga.user_position_map ( FOREIGN KEY (position_id) REFERENCES mjga.position(id) ON UPDATE NO ACTION ON DELETE RESTRICT ); +CREATE TYPE mjga.llm_code_enum AS ENUM ( + 'DEEP_SEEK', + 'ZHI_PU', + 'ZHI_PU_EMBEDDING' +); + +CREATE TYPE "llm_type_enum" AS ENUM ( + 'CHAT', + 'EMBEDDING' +); + CREATE TABLE mjga.ai_llm_config ( id BIGSERIAL NOT NULL UNIQUE, name VARCHAR(255) NOT NULL UNIQUE, + code mjga.llm_code_enum NOT NULL UNIQUE, + model_name VARCHAR(255) NOT NULL, + type LLM_TYPE_ENUM NOT NULL, api_key VARCHAR(255) NOT NULL, url VARCHAR(255) NOT NULL, enable BOOLEAN NOT NULL DEFAULT true, diff --git a/frontend/src/api/schema/openapi.json b/frontend/src/api/schema/openapi.json index 8f2039a..b53b6ec 100644 --- a/frontend/src/api/schema/openapi.json +++ b/frontend/src/api/schema/openapi.json @@ -747,6 +747,39 @@ } } }, + "/ai/action/chat": { + "post": { + "tags": [ + "ai-controller" + ], + "operationId": "actionChat", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + } + }, "/scheduler/page-query": { "get": { "tags": [ @@ -1096,6 +1129,7 @@ "modelName", "name", "priority", + "type", "url" ], "type": "object", @@ -1110,6 +1144,9 @@ "modelName": { "type": "string" }, + "type": { + "type": "string" + }, "apiKey": { "type": "string" }, @@ -1788,6 +1825,9 @@ "properties": { "name": { "type": "string" + }, + "type": { + "type": "string" } } }, diff --git a/frontend/src/api/types/schema.d.ts b/frontend/src/api/types/schema.d.ts index 5bb5086..5142735 100644 --- a/frontend/src/api/types/schema.d.ts +++ b/frontend/src/api/types/schema.d.ts @@ -372,6 +372,22 @@ export interface paths { patch?: never; trace?: never; }; + "/ai/action/chat": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["actionChat"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/scheduler/page-query": { parameters: { query?: never; @@ -529,6 +545,7 @@ export interface components { id: number; name: string; modelName: string; + type: string; apiKey: string; url: string; enable: boolean; @@ -759,6 +776,7 @@ export interface components { }; LlmQueryDto: { name?: string; + type?: string; }; PageResponseDtoListLlmVm: { /** Format: int64 */ @@ -1446,6 +1464,32 @@ export interface operations { }; }; }; + actionChat: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": string; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": { + [key: string]: string; + }; + }; + }; + }; + }; pageQuery: { parameters: { query: { diff --git a/frontend/src/components/DepartmentUpsertModal.vue b/frontend/src/components/DepartmentUpsertModal.vue index ab937e2..afd0a24 100644 --- a/frontend/src/components/DepartmentUpsertModal.vue +++ b/frontend/src/components/DepartmentUpsertModal.vue @@ -90,19 +90,9 @@ const handleSubmit = async () => { .max(15, "部门名称最多15个字符"), }); - try { - const validatedData = schema.parse(formData.value); - await onSubmit(validatedData); - updateFormData(undefined); - } catch (error) { - if (error instanceof z.ZodError) { - alertStore.showAlert({ - level: "error", - content: error.errors[0].message, - }); - } - throw error; - } + const validatedData = schema.parse(formData.value); + await onSubmit(validatedData); + updateFormData(undefined); }; onMounted(() => { diff --git a/frontend/src/components/LlmUpdateModal.vue b/frontend/src/components/LlmUpdateModal.vue index 40efce4..d4dadfe 100644 --- a/frontend/src/components/LlmUpdateModal.vue +++ b/frontend/src/components/LlmUpdateModal.vue @@ -34,6 +34,14 @@ class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 " required /> +
+ + +
llm, updateFormData, { }); const handleSubmit = async () => { - try { - const llmSchema = z.object({ - id: z.number({ - message: "id不能为空", - }), - name: z.string({ - message: "名称不能为空", - }), - modelName: z.string({ - message: "模型名称不能为空", - }), - apiKey: z.string({ - message: "apiKey不能为空", - }), - url: z.string({ - message: "url不能为空", - }), - enable: z.boolean({ - message: "状态不能为空", - }), - priority: z.number({ - message: "优先级必须为数字", - }), - }); - const validatedData = llmSchema.parse(formData.value); - await onSubmit(validatedData); - updateFormData(undefined); - } catch (error) { - if (error instanceof z.ZodError) { - alertStore.showAlert({ - level: "error", - content: error.errors[0].message, - }); - } - throw error; - } + const llmSchema = z.object({ + id: z.number({ + message: "id不能为空", + }), + name: z.string({ + message: "名称不能为空", + }), + modelName: z.string({ + message: "模型名称不能为空", + }), + apiKey: z.string({ + message: "apiKey不能为空", + }), + url: z.string({ + message: "url不能为空", + }), + enable: z.boolean({ + message: "状态不能为空", + }), + priority: z.number({ + message: "优先级必须为数字", + }), + type: z.string({ + message: "类型不能为空", + }), + }); + const validatedData = llmSchema.parse(formData.value); + await onSubmit(validatedData); + updateFormData(undefined); }; onMounted(() => { diff --git a/frontend/src/components/PermissionUpsertModal.vue b/frontend/src/components/PermissionUpsertModal.vue index f468a5c..82c8d58 100644 --- a/frontend/src/components/PermissionUpsertModal.vue +++ b/frontend/src/components/PermissionUpsertModal.vue @@ -42,7 +42,6 @@ diff --git a/frontend/src/components/PopupModal.vue b/frontend/src/components/PopupModal.vue index 53be8ac..4731ef8 100644 --- a/frontend/src/components/PopupModal.vue +++ b/frontend/src/components/PopupModal.vue @@ -12,12 +12,12 @@ Close modal
-

+

{{ title }}

diff --git a/frontend/src/components/PositionUpsertModal.vue b/frontend/src/components/PositionUpsertModal.vue index 9df1b04..1fbca84 100644 --- a/frontend/src/components/PositionUpsertModal.vue +++ b/frontend/src/components/PositionUpsertModal.vue @@ -78,19 +78,9 @@ const handleSubmit = async () => { .max(15, "岗位名称最多15个字符"), }); - try { - const validatedData = schema.parse(formData.value); - await onSubmit(validatedData); - updateFormData(undefined); - } catch (error) { - if (error instanceof z.ZodError) { - alertStore.showAlert({ - level: "error", - content: error.errors[0].message, - }); - } - throw error; - } + const validatedData = schema.parse(formData.value); + await onSubmit(validatedData); + updateFormData(undefined); }; onMounted(() => { diff --git a/frontend/src/components/RoleUpsertModal.vue b/frontend/src/components/RoleUpsertModal.vue index 7f1a493..b1f87da 100644 --- a/frontend/src/components/RoleUpsertModal.vue +++ b/frontend/src/components/RoleUpsertModal.vue @@ -86,19 +86,9 @@ const handleSubmit = async () => { .max(15, "角色代码最多15个字符"), }); - try { - const validatedData = roleSchema.parse(formData.value); - await onSubmit(validatedData); - updateFormData(undefined); - } catch (error) { - if (error instanceof z.ZodError) { - alertStore.showAlert({ - level: "error", - content: error.errors[0].message, - }); - } - throw error; - } + const validatedData = roleSchema.parse(formData.value); + await onSubmit(validatedData); + updateFormData(undefined); }; onMounted(() => { diff --git a/frontend/src/components/SchedulerUpdateModal.vue b/frontend/src/components/SchedulerUpdateModal.vue index 07130f4..d154cc8 100644 --- a/frontend/src/components/SchedulerUpdateModal.vue +++ b/frontend/src/components/SchedulerUpdateModal.vue @@ -69,17 +69,7 @@ const handleSubmit = async () => { .min(5, "表达式的长度非法"), }); - try { - const validatedData = jobSchema.parse(formData.value); - await onSubmit(validatedData.cronExpression); - } catch (error) { - if (error instanceof z.ZodError) { - alertStore.showAlert({ - level: "error", - content: error.errors[0].message, - }); - } - throw error; - } + const validatedData = jobSchema.parse(formData.value); + await onSubmit(validatedData.cronExpression); }; diff --git a/frontend/src/components/UserUpsertModal.vue b/frontend/src/components/UserUpsertModal.vue index d5c87a2..75fe7f4 100644 --- a/frontend/src/components/UserUpsertModal.vue +++ b/frontend/src/components/UserUpsertModal.vue @@ -61,15 +61,12 @@