mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-04-10 16:07:06 +00:00
init ai backend
This commit is contained in:
@@ -59,6 +59,9 @@ dependencies {
|
|||||||
implementation("org.flywaydb:flyway-database-postgresql:$flywayVersion")
|
implementation("org.flywaydb:flyway-database-postgresql:$flywayVersion")
|
||||||
implementation("com.github.ben-manes.caffeine:caffeine:3.2.0")
|
implementation("com.github.ben-manes.caffeine:caffeine:3.2.0")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-quartz")
|
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:junit-jupiter:$testcontainersVersion")
|
||||||
testImplementation("org.testcontainers:postgresql:$testcontainersVersion")
|
testImplementation("org.testcontainers:postgresql:$testcontainersVersion")
|
||||||
testImplementation("org.testcontainers:testcontainers-bom:$testcontainersVersion")
|
testImplementation("org.testcontainers:testcontainers-bom:$testcontainersVersion")
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ public class WebSecurityConfig {
|
|||||||
return new OrRequestMatcher(
|
return new OrRequestMatcher(
|
||||||
new AntPathRequestMatcher("/auth/sign-in", HttpMethod.POST.name()),
|
new AntPathRequestMatcher("/auth/sign-in", HttpMethod.POST.name()),
|
||||||
new AntPathRequestMatcher("/auth/sign-up", 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("/v3/api-docs/**", HttpMethod.GET.name()),
|
||||||
new AntPathRequestMatcher("/swagger-ui/**", HttpMethod.GET.name()),
|
new AntPathRequestMatcher("/swagger-ui/**", HttpMethod.GET.name()),
|
||||||
new AntPathRequestMatcher("/swagger-ui.html", HttpMethod.GET.name()),
|
new AntPathRequestMatcher("/swagger-ui.html", HttpMethod.GET.name()),
|
||||||
|
|||||||
@@ -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<String> chat(@RequestBody String userMessage) {
|
||||||
|
Sinks.Many<String> 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.zl.mjga.dto.urp;
|
package com.zl.mjga.dto.urp;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
backend/src/main/resources/ai.yml
Normal file
6
backend/src/main/resources/ai.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
deep-seek:
|
||||||
|
base-url: "https://api.deepseek.com"
|
||||||
|
api-key: "sk-3633b0cd40884b27aa8402a1c5dc029d"
|
||||||
|
model-name: "deepseek-chat"
|
||||||
|
prompt:
|
||||||
|
system: "你是一个名叫「知路智能体」的企业级AI助手,能帮助用户解决各种问题。"
|
||||||
@@ -15,6 +15,8 @@ cors:
|
|||||||
allowedHeaders: ${ALLOWED_HEADERS}
|
allowedHeaders: ${ALLOWED_HEADERS}
|
||||||
allowedExposeHeaders: ${ALLOWED_EXPOSE_HEADERS}
|
allowedExposeHeaders: ${ALLOWED_EXPOSE_HEADERS}
|
||||||
spring:
|
spring:
|
||||||
|
config:
|
||||||
|
import: classpath:ai.yml
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:postgresql://${DATABASE_HOST_PORT}/${DATABASE_DB}
|
url: jdbc:postgresql://${DATABASE_HOST_PORT}/${DATABASE_DB}
|
||||||
username: ${DATABASE_USER}
|
username: ${DATABASE_USER}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import com.zl.mjga.repository.PermissionRepository;
|
|||||||
import com.zl.mjga.repository.RoleRepository;
|
import com.zl.mjga.repository.RoleRepository;
|
||||||
import com.zl.mjga.repository.UserRepository;
|
import com.zl.mjga.repository.UserRepository;
|
||||||
import com.zl.mjga.service.IdentityAccessService;
|
import com.zl.mjga.service.IdentityAccessService;
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -58,21 +57,22 @@ public class JacksonAnnotationMvcTest {
|
|||||||
.andExpect(jsonPath("$.data[0].password").doesNotExist());
|
.andExpect(jsonPath("$.data[0].password").doesNotExist());
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Test
|
// @Test
|
||||||
// @WithMockUser
|
// @WithMockUser
|
||||||
// void dateFieldWithFormatAnnotation_whenResponseIncludeField_fieldShouldBeExpectDataFormat()
|
// void dateFieldWithFormatAnnotation_whenResponseIncludeField_fieldShouldBeExpectDataFormat()
|
||||||
// throws Exception {
|
// throws Exception {
|
||||||
// OffsetDateTime stubCreateDateTime =
|
// OffsetDateTime stubCreateDateTime =
|
||||||
// OffsetDateTime.of(2023, 12, 2, 1, 1, 1, 0, OffsetDateTime.now().getOffset());
|
// OffsetDateTime.of(2023, 12, 2, 1, 1, 1, 0, OffsetDateTime.now().getOffset());
|
||||||
// UserRolePermissionDto stubUserRolePermissionDto = new UserRolePermissionDto();
|
// UserRolePermissionDto stubUserRolePermissionDto = new UserRolePermissionDto();
|
||||||
// stubUserRolePermissionDto.setCreateTime(stubCreateDateTime);
|
// stubUserRolePermissionDto.setCreateTime(stubCreateDateTime);
|
||||||
// when(identityAccessService.pageQueryUser(any(PageRequestDto.class), any(UserQueryDto.class)))
|
// when(identityAccessService.pageQueryUser(any(PageRequestDto.class),
|
||||||
// .thenReturn(new PageResponseDto<>(1, List.of(stubUserRolePermissionDto)));
|
// any(UserQueryDto.class)))
|
||||||
// mockMvc
|
// .thenReturn(new PageResponseDto<>(1, List.of(stubUserRolePermissionDto)));
|
||||||
// .perform(
|
// mockMvc
|
||||||
// get(String.format("/iam/users?page=1&size=5&username=%s", "7bF3mcNVTj6P6v2"))
|
// .perform(
|
||||||
// .contentType(MediaType.APPLICATION_FORM_URLENCODED))
|
// get(String.format("/iam/users?page=1&size=5&username=%s", "7bF3mcNVTj6P6v2"))
|
||||||
// .andExpect(status().isOk())
|
// .contentType(MediaType.APPLICATION_FORM_URLENCODED))
|
||||||
// .andExpect(jsonPath("$.data[0].createTime").value("2023-12-02 01:01:01"));
|
// .andExpect(status().isOk())
|
||||||
// }
|
// .andExpect(jsonPath("$.data[0].createTime").value("2023-12-02 01:01:01"));
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user