feat(ai): 实现AI聊天功能并集成数据库工具
- 新增 AiChatController 支持流式聊天响应 - 创建 AIResponse 和 ChatMessageDTO 用于数据传输 - 开发 AiDBTools 提供用户相关的增删改查及封禁功能- 配置 ChatClient 支持默认工具调用 - 调整 User 实体类时间字段为 OffsetDateTime 并格式化- 添加 jackson-datatype-jsr310 依赖以支持 JSR310 时间序列化 - 修改 PostgreSQL 连接字符串时区配置 - 启用 Jackson 日期写入为字符串而非时间戳
This commit is contained in:
4
pom.xml
4
pom.xml
@@ -79,6 +79,10 @@
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-jsqlparser</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
<dependencyManagement>
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.hanserwei.chat.config;
|
||||
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
|
||||
import com.alibaba.cloud.ai.memory.redis.BaseRedisChatMemoryRepository;
|
||||
import com.alibaba.cloud.ai.memory.redis.LettuceRedisChatMemoryRepository;
|
||||
import com.hanserwei.chat.tools.AiDBTools;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor;
|
||||
@@ -28,6 +29,8 @@ public class ChatClientConfiguration {
|
||||
|
||||
@Resource
|
||||
private DashScopeChatModel dashScopeChatModel;
|
||||
@Resource
|
||||
private AiDBTools aiDBTools;
|
||||
|
||||
@Bean
|
||||
public BaseRedisChatMemoryRepository redisChatMemoryRepository() {
|
||||
@@ -51,6 +54,7 @@ public class ChatClientConfiguration {
|
||||
@Bean
|
||||
public ChatClient dashScopeChatClient(ChatMemory chatMemory) {
|
||||
return ChatClient.builder(dashScopeChatModel)
|
||||
.defaultTools(aiDBTools)
|
||||
.defaultAdvisors(PromptChatMemoryAdvisor.builder(chatMemory).build(), new SimpleLoggerAdvisor())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.hanserwei.chat.controller;
|
||||
|
||||
import com.hanserwei.chat.model.dto.ChatMessageDTO;
|
||||
import com.hanserwei.chat.model.vo.AIResponse;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.memory.ChatMemory;
|
||||
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;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/ai")
|
||||
public class AiChatController {
|
||||
|
||||
@Resource
|
||||
private ChatClient dashScopeChatClient;
|
||||
|
||||
@PostMapping(path = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public Flux<AIResponse> chatWithAi(@RequestBody ChatMessageDTO chatMessageDTO) {
|
||||
|
||||
return dashScopeChatClient.prompt()
|
||||
.user(chatMessageDTO.getMessage())
|
||||
.advisors(p -> p.param(ChatMemory.CONVERSATION_ID, chatMessageDTO.getConversionId()))
|
||||
.stream()
|
||||
.chatResponse()
|
||||
.mapNotNull(chatResponse -> AIResponse.builder()
|
||||
.v(chatResponse.getResult().getOutput().getText())
|
||||
.build());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,12 +4,13 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@@ -30,7 +31,8 @@ public class User {
|
||||
private Integer age;
|
||||
|
||||
@TableField(value = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssXXX")
|
||||
private OffsetDateTime createdAt;
|
||||
|
||||
@TableField(value = "is_active")
|
||||
private Boolean isActive;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.hanserwei.chat.model.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public class ChatMessageDTO {
|
||||
|
||||
private String message;
|
||||
|
||||
private Long conversionId;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.hanserwei.chat.model.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class AIResponse {
|
||||
// 流式响应内容
|
||||
private String v;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.hanserwei.chat.tools;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.hanserwei.chat.domain.dataobject.User;
|
||||
import com.hanserwei.chat.service.UserService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class AiDBTools {
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
@Tool(name = "findAll", description = "查询所有用户")
|
||||
public List<User> findAll() {
|
||||
return userService.list();
|
||||
}
|
||||
|
||||
@Tool(name = "findAllByIdIn", description = "根据id列表查询用户")
|
||||
public List<User> findAllByIdIn(@ToolParam(description = "用户id列表") List<Long> ids) {
|
||||
return userService.listByIds(ids);
|
||||
}
|
||||
|
||||
@Tool(name = "findById", description = "根据id查询用户")
|
||||
public User findById(Long id) {
|
||||
return userService.getById(id);
|
||||
}
|
||||
|
||||
@Tool(name = "findByName", description = "根据名称查询用户")
|
||||
public User findByName(String name) {
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(User.class)
|
||||
.eq(User::getName, name);
|
||||
return userService.getOne(queryWrapper);
|
||||
}
|
||||
|
||||
@Tool(name = "findByNameLike", description = "根据名称模糊查询用户")
|
||||
public List<User> findByNameLike(String name) {
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(User.class)
|
||||
.like(User::getName, name);
|
||||
return userService.list(queryWrapper);
|
||||
}
|
||||
|
||||
@Tool(name = "findByAge", description = "根据年龄查询用户")
|
||||
public List<User> findByAge(Integer age) {
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(User.class)
|
||||
.eq(User::getAge, age);
|
||||
return userService.list(queryWrapper);
|
||||
}
|
||||
|
||||
@Tool(name = "findByAgeBetween", description = "根据年龄范围查询用户")
|
||||
public List<User> findByAgeBetween(Integer start, Integer end) {
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(User.class)
|
||||
.between(User::getAge, start, end);
|
||||
return userService.list(queryWrapper);
|
||||
}
|
||||
|
||||
// 插入 数据
|
||||
@Tool(name = "insert",
|
||||
description = """
|
||||
插入一个新的用户。
|
||||
需要一个用户对象作为参数,
|
||||
该对象必须包含以下字段:
|
||||
name (String), email (String), 和 age (Integer)。
|
||||
""")
|
||||
public void insert(@ToolParam(description = "用户对象") User user) {
|
||||
userService.save(user);
|
||||
}
|
||||
|
||||
@Tool(name = "update",
|
||||
description = """
|
||||
更新现有用户。需要一个用户对象作为参数,该对象 **必须包含用户 ID**,
|
||||
并携带要修改的字段,例如 name (String), email (String), 或 age (Integer)。
|
||||
""")
|
||||
public void update(@ToolParam(description = "用户对象") User user) {
|
||||
userService.updateById(user);
|
||||
}
|
||||
|
||||
@Tool(name = "delete", description = "删除用户")
|
||||
public void delete(Long id) {
|
||||
userService.removeById(id);
|
||||
}
|
||||
|
||||
//封禁
|
||||
@Tool(name = "ban", description = "根据用户ID封禁用户。")
|
||||
public void ban(@ToolParam(description = "用户id") Long id) {
|
||||
userService.update(User.builder()
|
||||
.isActive(false)
|
||||
.build(),
|
||||
new LambdaQueryWrapper<>(User.class)
|
||||
.eq(User::getId, id));
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,9 @@ spring:
|
||||
name: snails-ai
|
||||
banner:
|
||||
location: config/banner.txt
|
||||
jackson:
|
||||
serialization:
|
||||
write-dates-as-timestamps: false
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
@@ -25,7 +28,7 @@ spring:
|
||||
time-between-eviction-runs: 10000
|
||||
datasource:
|
||||
driver-class-name: org.postgresql.Driver
|
||||
url: jdbc:postgresql://localhost:5432/postgres
|
||||
url: jdbc:postgresql://localhost:5432/postgres?serverTimezone=Asia/Shanghai
|
||||
username: postgres
|
||||
password: postgressql
|
||||
# HikariCP 连接池配置
|
||||
|
||||
Reference in New Issue
Block a user