diff --git a/backend/src/main/java/com/zl/mjga/config/ai/DeepSeekConfiguration.java b/backend/src/main/java/com/zl/mjga/config/ai/DeepSeekConfiguration.java index fcc3fed..bccfd91 100644 --- a/backend/src/main/java/com/zl/mjga/config/ai/DeepSeekConfiguration.java +++ b/backend/src/main/java/com/zl/mjga/config/ai/DeepSeekConfiguration.java @@ -1,15 +1,29 @@ package com.zl.mjga.config.ai; +import com.zl.mjga.service.LlmService; +import jakarta.annotation.PostConstruct; import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; +import lombok.RequiredArgsConstructor; +import org.jooq.generated.default_schema.enums.LlmCodeEnum; +import org.jooq.generated.mjga.tables.pojos.AiLlmConfig; import org.springframework.stereotype.Component; @Data @Component -@ConfigurationProperties(prefix = "deep-seek") +@RequiredArgsConstructor public class DeepSeekConfiguration { private String baseUrl; private String apiKey; private String modelName; + + private final LlmService llmService; + + @PostConstruct + public void init() { + AiLlmConfig aiLlmConfig = llmService.loadConfig(LlmCodeEnum.DEEP_SEEK); + baseUrl = aiLlmConfig.getUrl(); + apiKey = aiLlmConfig.getApiKey(); + modelName = aiLlmConfig.getModelName(); + } } diff --git a/backend/src/main/java/com/zl/mjga/config/ai/ZhiPuConfiguration.java b/backend/src/main/java/com/zl/mjga/config/ai/ZhiPuConfiguration.java index ea659a1..1746653 100644 --- a/backend/src/main/java/com/zl/mjga/config/ai/ZhiPuConfiguration.java +++ b/backend/src/main/java/com/zl/mjga/config/ai/ZhiPuConfiguration.java @@ -1,15 +1,27 @@ package com.zl.mjga.config.ai; +import com.zl.mjga.service.LlmService; +import jakarta.annotation.PostConstruct; import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; +import org.jooq.generated.default_schema.enums.LlmCodeEnum; +import org.jooq.generated.mjga.tables.pojos.AiLlmConfig; import org.springframework.stereotype.Component; @Data @Component -@ConfigurationProperties(prefix = "zhipu") public class ZhiPuConfiguration { private String baseUrl; private String apiKey; private String modelName; + + private final LlmService llmService; + + @PostConstruct + public void init() { + AiLlmConfig aiLlmConfig = llmService.loadConfig(LlmCodeEnum.ZHI_PU); + baseUrl = aiLlmConfig.getUrl(); + apiKey = aiLlmConfig.getApiKey(); + modelName = aiLlmConfig.getModelName(); + } } 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 3c038d1..e724b78 100644 --- a/backend/src/main/java/com/zl/mjga/controller/AiController.java +++ b/backend/src/main/java/com/zl/mjga/controller/AiController.java @@ -1,7 +1,10 @@ package com.zl.mjga.controller; +import com.zl.mjga.dto.ai.LlmUpdateDto; import com.zl.mjga.service.AiChatService; +import com.zl.mjga.service.LlmService; import dev.langchain4j.service.TokenStream; +import jakarta.validation.Valid; import java.security.Principal; import java.time.Duration; import lombok.RequiredArgsConstructor; @@ -18,11 +21,12 @@ import reactor.core.publisher.Sinks; public class AiController { private final AiChatService aiChatService; + private final LlmService llmService; @PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux chat(Principal principal, @RequestBody String userMessage) { Sinks.Many sink = Sinks.many().unicast().onBackpressureBuffer(); - TokenStream chat = aiChatService.chatWithZhiPu(principal.getName(), userMessage); + TokenStream chat = aiChatService.chatPrecedenceLlmWith(principal.getName(), userMessage); chat.onPartialResponse(text -> sink.tryEmitNext(text.replace(" ", "␣").replace("\t", "⇥"))) .onCompleteResponse( r -> { @@ -33,4 +37,9 @@ public class AiController { .start(); return sink.asFlux().timeout(Duration.ofSeconds(120)); } + + @PutMapping(value = "/llm") + public void updateLlm(@RequestBody @Valid LlmUpdateDto llmUpdateDto) { + llmService.update(llmUpdateDto); + } } diff --git a/backend/src/main/java/com/zl/mjga/dto/ai/LlmUpdateDto.java b/backend/src/main/java/com/zl/mjga/dto/ai/LlmUpdateDto.java new file mode 100644 index 0000000..66c47ac --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/ai/LlmUpdateDto.java @@ -0,0 +1,16 @@ +package com.zl.mjga.dto.ai; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class LlmUpdateDto { + @NotNull private Long id; + @NotEmpty private String name; + @NotEmpty private String modelName; + @NotEmpty private String apiKey; + @NotEmpty private String url; + @NotNull private Boolean enable; + @NotNull private Short priority; +} 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 08ae6a9..63ccfc2 100644 --- a/backend/src/main/java/com/zl/mjga/service/AiChatService.java +++ b/backend/src/main/java/com/zl/mjga/service/AiChatService.java @@ -4,6 +4,8 @@ import com.zl.mjga.config.ai.AiChatAssistant; import dev.langchain4j.service.TokenStream; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.jooq.generated.default_schema.enums.LlmCodeEnum; +import org.jooq.generated.mjga.tables.pojos.AiLlmConfig; import org.springframework.stereotype.Service; @Service @@ -13,6 +15,7 @@ public class AiChatService { private final AiChatAssistant deepSeekChatAssistant; private final AiChatAssistant zhiPuChatAssistant; + private final LlmService llmService; public TokenStream chatWithDeepSeek(String sessionIdentifier, String userMessage) { return deepSeekChatAssistant.chat(sessionIdentifier, userMessage); @@ -21,4 +24,13 @@ public class AiChatService { public TokenStream chatWithZhiPu(String sessionIdentifier, String userMessage) { return zhiPuChatAssistant.chat(sessionIdentifier, userMessage); } + + public TokenStream chatPrecedenceLlmWith(String sessionIdentifier, String userMessage) { + AiLlmConfig precedenceLlmBy = llmService.getPrecedenceLlmBy(true); + LlmCodeEnum code = precedenceLlmBy.getCode(); + return switch (code) { + case ZHI_PU -> zhiPuChatAssistant.chat(sessionIdentifier, userMessage); + case DEEP_SEEK -> deepSeekChatAssistant.chat(sessionIdentifier, userMessage); + }; + } } diff --git a/backend/src/main/java/com/zl/mjga/service/LlmService.java b/backend/src/main/java/com/zl/mjga/service/LlmService.java new file mode 100644 index 0000000..b730efd --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/service/LlmService.java @@ -0,0 +1,41 @@ +package com.zl.mjga.service; + +import com.zl.mjga.dto.ai.LlmUpdateDto; +import java.util.List; +import java.util.Objects; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jooq.generated.default_schema.enums.LlmCodeEnum; +import org.jooq.generated.mjga.tables.daos.AiLlmConfigDao; +import org.jooq.generated.mjga.tables.pojos.AiLlmConfig; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class LlmService { + + private final AiLlmConfigDao aiLlmConfigDao; + + public AiLlmConfig loadConfig(LlmCodeEnum llmCodeEnum) { + return aiLlmConfigDao.fetchOneByCode(llmCodeEnum); + } + + public AiLlmConfig getPrecedenceLlmBy(Boolean enable) { + List aiLlmConfigs = aiLlmConfigDao.fetchByEnable(enable); + //noinspection OptionalGetWithoutIsPresent + return aiLlmConfigs.stream() + .max((o1, o2) -> o2.getPriority().compareTo(o1.getPriority())) + .get(); + } + + public void update(LlmUpdateDto llmUpdateDto) { + AiLlmConfig aiLlmConfig = new AiLlmConfig(); + BeanUtils.copyProperties(llmUpdateDto, aiLlmConfig); + AiLlmConfig byId = aiLlmConfigDao.findById(llmUpdateDto.getId()); + aiLlmConfig.setCode(Objects.requireNonNull(byId).getCode()); + aiLlmConfigDao.merge(aiLlmConfig); + } +} diff --git a/backend/src/main/resources/ai.yml b/backend/src/main/resources/ai.yml deleted file mode 100644 index 0df5419..0000000 --- a/backend/src/main/resources/ai.yml +++ /dev/null @@ -1,8 +0,0 @@ -deep-seek: - base-url: "https://api.deepseek.com" - api-key: "" - model-name: "deepseek-chat" -zhipu: - base-url: "https://open.bigmodel.cn/" - api-key: "" - model-name: "glm-4-flash" \ No newline at end of file diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 078fad4..56c3477 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -15,8 +15,6 @@ cors: allowedHeaders: ${ALLOWED_HEADERS} allowedExposeHeaders: ${ALLOWED_EXPOSE_HEADERS} spring: - config: - import: classpath:ai.yml datasource: url: jdbc:postgresql://${DATABASE_HOST_PORT}/${DATABASE_DB} username: ${DATABASE_USER} 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 2cef2f3..2859c0e 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 @@ -64,4 +64,22 @@ CREATE TABLE mjga.user_position_map ( PRIMARY KEY (user_id, position_id), FOREIGN KEY (user_id) REFERENCES mjga.user(id) ON UPDATE NO ACTION ON DELETE RESTRICT, FOREIGN KEY (position_id) REFERENCES mjga.position(id) ON UPDATE NO ACTION ON DELETE RESTRICT -); \ No newline at end of file +); + +CREATE TYPE "llm_code_enum" AS ENUM ( + 'DEEP_SEEK', + 'ZHI_PU' +); + + +CREATE TABLE mjga.ai_llm_config ( + id BIGSERIAL NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL UNIQUE, + code LLM_CODE_ENUM NOT NULL UNIQUE, + model_name VARCHAR(255) NOT NULL, + api_key VARCHAR(255) NOT NULL, + url VARCHAR(255) NOT NULL, + enable BOOLEAN NOT NULL DEFAULT true, + priority SMALLINT NOT NULL DEFAULT 0, + PRIMARY KEY(id) +); 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 81a5139..217ae54 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 @@ -27,3 +27,8 @@ VALUES (1, 1), (1, 6), (1, 7), (1, 8); + +INSERT INTO mjga.ai_llm_config (name,code,model_name, api_key, url, enable, priority) +VALUES + ('DeepSeek','DEEP_SEEK','deepseek-chat','', 'https://api.deepseek.com', false, 0), + ('智谱清言','ZHI_PU','glm-4-flash', '', 'https://open.bigmodel.cn/', false, 1); \ 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 2cef2f3..00dc253 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 @@ -64,4 +64,14 @@ CREATE TABLE mjga.user_position_map ( PRIMARY KEY (user_id, position_id), FOREIGN KEY (user_id) REFERENCES mjga.user(id) ON UPDATE NO ACTION ON DELETE RESTRICT, FOREIGN KEY (position_id) REFERENCES mjga.position(id) ON UPDATE NO ACTION ON DELETE RESTRICT -); \ No newline at end of file +); + +CREATE TABLE mjga.ai_llm_config ( + id BIGSERIAL NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL UNIQUE, + api_key VARCHAR(255) NOT NULL, + url VARCHAR(255) NOT NULL, + enable BOOLEAN NOT NULL DEFAULT true, + priority SMALLINT NOT NULL DEFAULT 0, + PRIMARY KEY(id) +);