From f103993960b189e087288ecca6917c05fd22221e Mon Sep 17 00:00:00 2001 From: Chuck1sn Date: Tue, 20 May 2025 21:35:45 +0800 Subject: [PATCH] init ai backend --- backend/build.gradle.kts | 3 ++ .../zl/mjga/config/ai/ChatModelConfig.java | 33 +++++++++++++++ .../mjga/config/ai/DeepSeekChatAssistant.java | 12 ++++++ .../mjga/config/ai/DeepSeekConfiguration.java | 21 ++++++++++ .../config/security/WebSecurityConfig.java | 1 + .../com/zl/mjga/controller/AiController.java | 41 +++++++++++++++++++ .../mjga/dto/urp/UserRolePermissionDto.java | 1 - .../zl/mjga/service/DeepSeekAiService.java | 19 +++++++++ backend/src/main/resources/ai.yml | 6 +++ backend/src/main/resources/application.yml | 2 + .../mvc/JacksonAnnotationMvcTest.java | 36 ++++++++-------- 11 files changed, 156 insertions(+), 19 deletions(-) create mode 100644 backend/src/main/java/com/zl/mjga/config/ai/ChatModelConfig.java create mode 100644 backend/src/main/java/com/zl/mjga/config/ai/DeepSeekChatAssistant.java create mode 100644 backend/src/main/java/com/zl/mjga/config/ai/DeepSeekConfiguration.java create mode 100644 backend/src/main/java/com/zl/mjga/controller/AiController.java create mode 100644 backend/src/main/java/com/zl/mjga/service/DeepSeekAiService.java create mode 100644 backend/src/main/resources/ai.yml diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index bd2850c..d902a29 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -59,6 +59,9 @@ dependencies { implementation("org.flywaydb:flyway-database-postgresql:$flywayVersion") implementation("com.github.ben-manes.caffeine:caffeine:3.2.0") implementation("org.springframework.boot:spring-boot-starter-quartz") + implementation("dev.langchain4j:langchain4j-open-ai:1.0.0") + implementation("io.projectreactor:reactor-core:3.7.6") + implementation("dev.langchain4j:langchain4j:1.0.0") testImplementation("org.testcontainers:junit-jupiter:$testcontainersVersion") testImplementation("org.testcontainers:postgresql:$testcontainersVersion") testImplementation("org.testcontainers:testcontainers-bom:$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/ChatModelConfig.java new file mode 100644 index 0000000..b8d57bb --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/config/ai/ChatModelConfig.java @@ -0,0 +1,33 @@ +package com.zl.mjga.config.ai; + +import dev.langchain4j.memory.chat.MessageWindowChatMemory; +import dev.langchain4j.model.openai.OpenAiStreamingChatModel; +import dev.langchain4j.service.AiServices; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class ChatModelConfig { + + private final DeepSeekConfiguration deepSeekConfiguration; + + @Bean + public OpenAiStreamingChatModel deepSeekChatModel() { + return OpenAiStreamingChatModel.builder() + .baseUrl(deepSeekConfiguration.getBaseUrl()) + .apiKey(deepSeekConfiguration.getApiKey()) + .modelName(deepSeekConfiguration.getModelName()) + .build(); + } + + @Bean + public DeepSeekChatAssistant deepSeekChatAssistant(OpenAiStreamingChatModel deepSeekChatModel) { + return AiServices.builder(DeepSeekChatAssistant.class) + .streamingChatModel(deepSeekChatModel) + .systemMessageProvider(chatMemoryId -> "你是一个叫做「知路 AI」的企业级 AI 助手,能帮助用户回答任何问题。") + .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) + .build(); + } +} diff --git a/backend/src/main/java/com/zl/mjga/config/ai/DeepSeekChatAssistant.java b/backend/src/main/java/com/zl/mjga/config/ai/DeepSeekChatAssistant.java new file mode 100644 index 0000000..75d34cf --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/config/ai/DeepSeekChatAssistant.java @@ -0,0 +1,12 @@ +package com.zl.mjga.config.ai; + +import dev.langchain4j.service.MemoryId; +import dev.langchain4j.service.SystemMessage; +import dev.langchain4j.service.TokenStream; +import dev.langchain4j.service.UserMessage; +import dev.langchain4j.service.memory.ChatMemoryAccess; + +public interface DeepSeekChatAssistant extends ChatMemoryAccess { + @SystemMessage("You are a good friend of mine. Answer using slang.") + TokenStream chat(@MemoryId String memoryId, @UserMessage String userMessage); +} 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 new file mode 100644 index 0000000..02b8b74 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/config/ai/DeepSeekConfiguration.java @@ -0,0 +1,21 @@ +package com.zl.mjga.config.ai; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "deep-seek") +public class DeepSeekConfiguration { + + private String baseUrl; + private String apiKey; + private Prompt prompt; + private String modelName; + + @Data + public static class Prompt { + private String system; + } +} diff --git a/backend/src/main/java/com/zl/mjga/config/security/WebSecurityConfig.java b/backend/src/main/java/com/zl/mjga/config/security/WebSecurityConfig.java index b7f2348..2ac8d7d 100644 --- a/backend/src/main/java/com/zl/mjga/config/security/WebSecurityConfig.java +++ b/backend/src/main/java/com/zl/mjga/config/security/WebSecurityConfig.java @@ -39,6 +39,7 @@ public class WebSecurityConfig { return new OrRequestMatcher( new AntPathRequestMatcher("/auth/sign-in", HttpMethod.POST.name()), new AntPathRequestMatcher("/auth/sign-up", HttpMethod.POST.name()), + new AntPathRequestMatcher("/ai/**", HttpMethod.POST.name()), new AntPathRequestMatcher("/v3/api-docs/**", HttpMethod.GET.name()), new AntPathRequestMatcher("/swagger-ui/**", HttpMethod.GET.name()), new AntPathRequestMatcher("/swagger-ui.html", HttpMethod.GET.name()), diff --git a/backend/src/main/java/com/zl/mjga/controller/AiController.java b/backend/src/main/java/com/zl/mjga/controller/AiController.java new file mode 100644 index 0000000..4dc2d63 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/controller/AiController.java @@ -0,0 +1,41 @@ +package com.zl.mjga.controller; + +import com.zl.mjga.service.DeepSeekAiService; +import dev.langchain4j.service.TokenStream; +import java.time.Duration; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Sinks; + +@RestController +@RequestMapping("/ai") +@RequiredArgsConstructor +@Slf4j +public class AiController { + + private final DeepSeekAiService deepSeekAiService; + + @PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux chat(@RequestBody String userMessage) { + Sinks.Many sink = Sinks.many().unicast().onBackpressureBuffer(); + TokenStream chat = deepSeekAiService.chat("123", userMessage); + chat.onPartialResponse(sink::tryEmitNext) + .onCompleteResponse( + r -> { + sink.tryEmitNext("[DONE]"); + sink.tryEmitComplete(); + }) + .onError(sink::tryEmitError) + .start(); + + return sink.asFlux() + .timeout(Duration.ofSeconds(60)) + .onErrorResume(e -> Flux.just("Timeout occurred")); + } +} diff --git a/backend/src/main/java/com/zl/mjga/dto/urp/UserRolePermissionDto.java b/backend/src/main/java/com/zl/mjga/dto/urp/UserRolePermissionDto.java index 0d648b9..5b253cf 100644 --- a/backend/src/main/java/com/zl/mjga/dto/urp/UserRolePermissionDto.java +++ b/backend/src/main/java/com/zl/mjga/dto/urp/UserRolePermissionDto.java @@ -1,6 +1,5 @@ package com.zl.mjga.dto.urp; -import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; import java.time.OffsetDateTime; import java.util.LinkedList; diff --git a/backend/src/main/java/com/zl/mjga/service/DeepSeekAiService.java b/backend/src/main/java/com/zl/mjga/service/DeepSeekAiService.java new file mode 100644 index 0000000..5bc818c --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/service/DeepSeekAiService.java @@ -0,0 +1,19 @@ +package com.zl.mjga.service; + +import com.zl.mjga.config.ai.DeepSeekChatAssistant; +import dev.langchain4j.service.TokenStream; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class DeepSeekAiService { + + private final DeepSeekChatAssistant deepSeekChatAssistant; + + public TokenStream chat(String sessionIdentifier, String userMessage) { + return deepSeekChatAssistant.chat(sessionIdentifier, userMessage); + } +} diff --git a/backend/src/main/resources/ai.yml b/backend/src/main/resources/ai.yml new file mode 100644 index 0000000..368bc8a --- /dev/null +++ b/backend/src/main/resources/ai.yml @@ -0,0 +1,6 @@ +deep-seek: + base-url: "https://api.deepseek.com" + api-key: "sk-3633b0cd40884b27aa8402a1c5dc029d" + model-name: "deepseek-chat" + prompt: + system: "你是一个名叫「知路智能体」的企业级AI助手,能帮助用户解决各种问题。" diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 56c3477..078fad4 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -15,6 +15,8 @@ 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/test/java/com/zl/mjga/integration/mvc/JacksonAnnotationMvcTest.java b/backend/src/test/java/com/zl/mjga/integration/mvc/JacksonAnnotationMvcTest.java index 6a70ff3..a0723df 100644 --- a/backend/src/test/java/com/zl/mjga/integration/mvc/JacksonAnnotationMvcTest.java +++ b/backend/src/test/java/com/zl/mjga/integration/mvc/JacksonAnnotationMvcTest.java @@ -15,7 +15,6 @@ import com.zl.mjga.repository.PermissionRepository; import com.zl.mjga.repository.RoleRepository; import com.zl.mjga.repository.UserRepository; import com.zl.mjga.service.IdentityAccessService; -import java.time.OffsetDateTime; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -58,21 +57,22 @@ public class JacksonAnnotationMvcTest { .andExpect(jsonPath("$.data[0].password").doesNotExist()); } -// @Test -// @WithMockUser -// void dateFieldWithFormatAnnotation_whenResponseIncludeField_fieldShouldBeExpectDataFormat() -// throws Exception { -// OffsetDateTime stubCreateDateTime = -// OffsetDateTime.of(2023, 12, 2, 1, 1, 1, 0, OffsetDateTime.now().getOffset()); -// UserRolePermissionDto stubUserRolePermissionDto = new UserRolePermissionDto(); -// stubUserRolePermissionDto.setCreateTime(stubCreateDateTime); -// when(identityAccessService.pageQueryUser(any(PageRequestDto.class), any(UserQueryDto.class))) -// .thenReturn(new PageResponseDto<>(1, List.of(stubUserRolePermissionDto))); -// mockMvc -// .perform( -// get(String.format("/iam/users?page=1&size=5&username=%s", "7bF3mcNVTj6P6v2")) -// .contentType(MediaType.APPLICATION_FORM_URLENCODED)) -// .andExpect(status().isOk()) -// .andExpect(jsonPath("$.data[0].createTime").value("2023-12-02 01:01:01")); -// } + // @Test + // @WithMockUser + // void dateFieldWithFormatAnnotation_whenResponseIncludeField_fieldShouldBeExpectDataFormat() + // throws Exception { + // OffsetDateTime stubCreateDateTime = + // OffsetDateTime.of(2023, 12, 2, 1, 1, 1, 0, OffsetDateTime.now().getOffset()); + // UserRolePermissionDto stubUserRolePermissionDto = new UserRolePermissionDto(); + // stubUserRolePermissionDto.setCreateTime(stubCreateDateTime); + // when(identityAccessService.pageQueryUser(any(PageRequestDto.class), + // any(UserQueryDto.class))) + // .thenReturn(new PageResponseDto<>(1, List.of(stubUserRolePermissionDto))); + // mockMvc + // .perform( + // get(String.format("/iam/users?page=1&size=5&username=%s", "7bF3mcNVTj6P6v2")) + // .contentType(MediaType.APPLICATION_FORM_URLENCODED)) + // .andExpect(status().isOk()) + // .andExpect(jsonPath("$.data[0].createTime").value("2023-12-02 01:01:01")); + // } }