智能客服

This commit is contained in:
liukai
2025-09-04 15:27:44 +08:00
parent a923cd73f7
commit d917c6ca85
24 changed files with 556 additions and 62 deletions

View File

@@ -44,11 +44,10 @@
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
<version>3.5.10.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>

View File

@@ -1,9 +1,11 @@
package com.ai.app;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan(basePackages = "com.ai.app.mapper")
public class SpringAiAppApplication {
public static void main(String[] args) {

View File

@@ -1,6 +1,7 @@
package com.ai.app.config;
import com.ai.app.constant.SystemConstants;
import com.ai.app.tools.CourseTools;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
@@ -78,7 +79,7 @@ public class AIConfig {
}
/**
* dashScopeChatClient对话客户端
* 游戏对话客户端
*
* @param dashScopeChatModel 千问max大模型
* @param simpleLoggerAdvisor 日志Advisor
@@ -101,6 +102,32 @@ public class AIConfig {
.build();
}
/**
* 智能客服chat client
*
* @param dashScopeChatModel 千问模型
* @param simpleLoggerAdvisor 日志增强
* @param messageChatMemoryAdvisor 对话记忆
* @return: org.springframework.ai.chat.client.ChatClient
* @author kai.liu
* @date: 2025/9/4 14:35
*/
@Bean
public ChatClient serviceDashScopeChatClient(DashScopeChatModel dashScopeChatModel,
SimpleLoggerAdvisor simpleLoggerAdvisor,
MessageChatMemoryAdvisor messageChatMemoryAdvisor,
CourseTools courseTools) {
return ChatClient
//模型
.builder(dashScopeChatModel)
//系统提示词
.defaultSystem(SystemConstants.SERVICE_SYSTEM_PROMPT)
//环绕增强
.defaultAdvisors(simpleLoggerAdvisor, messageChatMemoryAdvisor)
.defaultTools(courseTools)
.build();
}
/**
* deepseek 对话客户端
* 响应太慢,不建议使用

View File

@@ -1,41 +0,0 @@
package com.ai.app.config;
import com.ai.app.service.AIChatMessageService;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.messages.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @describe
* @Author kai.liu
* @Date 2025/8/29 9:01
*/
@Component
public class RedisChatMemoryRepositor implements ChatMemoryRepository {
@Autowired
private AIChatMessageService aiChatMessageService;
@Override
public List<String> findConversationIds() {
return aiChatMessageService.getConversationIds();
}
@Override
public List<Message> findByConversationId(String conversationId) {
return aiChatMessageService.getMessage(conversationId);
}
@Override
public void saveAll(String conversationId, List<Message> messages) {
aiChatMessageService.saveAll(conversationId, messages);
}
@Override
public void deleteByConversationId(String conversationId) {
aiChatMessageService.delete(conversationId);
}
}

View File

@@ -1,7 +1,6 @@
package com.ai.app.controller;
import com.ai.app.constant.ChatTypeEnum;
import com.ai.app.constant.SystemConstants;
import com.ai.app.dto.MessageDTO;
import com.ai.app.service.ChatHistoryService;
import org.springframework.ai.chat.client.ChatClient;
@@ -37,6 +36,10 @@ public class DashScopeController {
@Qualifier("gameDashScopeChatClient")
private ChatClient gameDashScopeChatClient;
@Autowired
@Qualifier("serviceDashScopeChatClient")
private ChatClient serviceDashScopeChatClient;
@Autowired
private ChatHistoryService chatHistoryService;
@@ -107,4 +110,23 @@ public class DashScopeController {
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId))
.stream().content();
}
/**
* it培训机构智能客服
*
* @param prompt 用户提示词
* @param chatId 对话ID
* @return: reactor.core.publisher.Flux<java.lang.String>
* @author kai.liu
* @date: 2025/9/4 14:51
*/
@RequestMapping(value = "/service", produces = "text/stream;charset=UTF-8")
public Flux<String> service(String prompt, String chatId) {
chatHistoryService.saveHistoryChatId(ChatTypeEnum.CHAT_PDF.type, chatId);
return serviceDashScopeChatClient
.prompt()
.user(prompt)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId))
.stream().content();
}
}

View File

@@ -0,0 +1,59 @@
package com.ai.app.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* <p>
* 学科表
* </p>
*
* @author huge
* @since 2025-03-08
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("course")
public class Course implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 学科名称
*/
private String name;
/**
* 学历背景要求0-无1-初中2-高中、3-大专、4-本科以上
*/
private Integer edu;
/**
* 课程类型:编程、设计、自媒体、其它
*/
private String type;
/**
* 课程价格
*/
private Long price;
/**
* 学习时长,单位: 天
*/
private Integer duration;
}

View File

@@ -0,0 +1,56 @@
package com.ai.app.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* <p>
*
* </p>
*
* @author huge
* @since 2025-03-08
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("course_reservation")
public class CourseReservation implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 预约课程
*/
private String course;
/**
* 学生姓名
*/
private String studentName;
/**
* 联系方式
*/
private String contactInfo;
/**
* 预约校区
*/
private String school;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,37 @@
package com.ai.app.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ai.chat.messages.*;
import java.util.List;
import java.util.Map;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Msg {
MessageType messageType;
String text;
Map<String, Object> metadata;
List<AssistantMessage.ToolCall> toolCalls;
public Msg(Message message) {
this.messageType = message.getMessageType();
this.text = message.getText();
this.metadata = message.getMetadata();
if (message instanceof AssistantMessage am) {
this.toolCalls = am.getToolCalls();
}
}
public Message toMessage() {
return switch (messageType) {
case SYSTEM -> new SystemMessage(text);
case USER -> UserMessage.builder().text(text).media(List.of()).metadata(metadata).build();
case ASSISTANT -> new AssistantMessage(text, metadata, toolCalls, List.of());
default -> throw new IllegalArgumentException("Unsupported message type: " + messageType);
};
}
}

View File

@@ -0,0 +1,44 @@
package com.ai.app.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* <p>
* 校区表
* </p>
*
* @author huge
* @since 2025-03-08
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("school")
public class School implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 校区名称
*/
private String name;
/**
* 校区所在城市
*/
private String city;
}

View File

@@ -0,0 +1,16 @@
package com.ai.app.mapper;
import com.ai.app.entity.Course;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 学科表 Mapper 接口
* </p>
*
* @author huge
* @since 2025-03-08
*/
public interface CourseMapper extends BaseMapper<Course> {
}

View File

@@ -0,0 +1,17 @@
package com.ai.app.mapper;
import com.ai.app.entity.CourseReservation;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author huge
* @since 2025-03-08
*/
public interface CourseReservationMapper extends BaseMapper<CourseReservation> {
}

View File

@@ -0,0 +1,16 @@
package com.ai.app.mapper;
import com.ai.app.entity.School;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 校区表 Mapper 接口
* </p>
*
* @author huge
* @since 2025-03-08
*/
public interface SchoolMapper extends BaseMapper<School> {
}

View File

@@ -0,0 +1,24 @@
package com.ai.app.query;
import lombok.Data;
import org.springframework.ai.tool.annotation.ToolParam;
import java.util.List;
@Data
public class CourseQuery {
@ToolParam(required = false, description = "课程类型:编程、设计、自媒体、其它")
private String type;
@ToolParam(required = false, description = "学历要求0-无、1-初中、2-高中、3-大专、4-本科及本科以上")
private Integer edu;
@ToolParam(required = false, description = "排序方式")
private List<Sort> sorts;
@Data
public static class Sort {
@ToolParam(required = false, description = "排序字段: price或duration")
private String field;
@ToolParam(required = false, description = "是否是升序: true/false")
private Boolean asc;
}
}

View File

@@ -0,0 +1,16 @@
package com.ai.app.service;
import com.ai.app.entity.CourseReservation;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 服务类
* </p>
*
* @author huge
* @since 2025-03-08
*/
public interface ICourseReservationService extends IService<CourseReservation> {
}

View File

@@ -0,0 +1,16 @@
package com.ai.app.service;
import com.ai.app.entity.Course;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 学科表 服务类
* </p>
*
* @author huge
* @since 2025-03-08
*/
public interface ICourseService extends IService<Course> {
}

View File

@@ -0,0 +1,16 @@
package com.ai.app.service;
import com.ai.app.entity.School;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 校区表 服务类
* </p>
*
* @author huge
* @since 2025-03-08
*/
public interface ISchoolService extends IService<School> {
}

View File

@@ -7,7 +7,9 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @describe
@@ -18,10 +20,11 @@ import java.util.List;
@Service
public class ChatHistoryServiceImpl implements ChatHistoryService {
@Autowired
private RedisTemplateService redisTemplateService;
/*@Autowired
private RedisTemplateService redisTemplateService;*/
private final Map<String,List<String>> chatStore = new HashMap<>();
public static final String CHAT_TYPE_KEY = "ai:history:type:";
/**
* 保存会话ID
@@ -34,18 +37,11 @@ public class ChatHistoryServiceImpl implements ChatHistoryService {
*/
@Override
public void saveHistoryChatId(String type, String chatId) {
String key = CHAT_TYPE_KEY + type;
List<String> chatIds;
if (redisTemplateService.hasKey(key)) {
chatIds = (List<String>) redisTemplateService.get(key);
} else {
chatIds = new ArrayList<>();
}
List<String> chatIds = chatStore.computeIfAbsent(type, k -> new ArrayList<>());
if (chatIds.contains(chatId)) {
return;
}
chatIds.add(chatId);
redisTemplateService.set(key, chatIds);
}
/**
@@ -58,10 +54,6 @@ public class ChatHistoryServiceImpl implements ChatHistoryService {
*/
@Override
public List<String> getHistoryChatIds(String type) {
String key = CHAT_TYPE_KEY + type;
if (redisTemplateService.hasKey(key)) {
return (List<String>) redisTemplateService.get(key);
}
return List.of();
return chatStore.getOrDefault(type, List.of());
}
}

View File

@@ -0,0 +1,20 @@
package com.ai.app.service.impl;
import com.ai.app.entity.CourseReservation;
import com.ai.app.mapper.CourseReservationMapper;
import com.ai.app.service.ICourseReservationService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 服务实现类
* </p>
*
* @author huge
* @since 2025-03-08
*/
@Service
public class CourseReservationServiceImpl extends ServiceImpl<CourseReservationMapper, CourseReservation> implements ICourseReservationService {
}

View File

@@ -0,0 +1,20 @@
package com.ai.app.service.impl;
import com.ai.app.entity.Course;
import com.ai.app.mapper.CourseMapper;
import com.ai.app.service.ICourseService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 学科表 服务实现类
* </p>
*
* @author huge
* @since 2025-03-08
*/
@Service
public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> implements ICourseService {
}

View File

@@ -0,0 +1,21 @@
package com.ai.app.service.impl;
import com.ai.app.entity.School;
import com.ai.app.mapper.SchoolMapper;
import com.ai.app.service.ISchoolService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 校区表 服务实现类
* </p>
*
* @author huge
* @since 2025-03-08
*/
@Service
public class SchoolServiceImpl extends ServiceImpl<SchoolMapper, School> implements ISchoolService {
}

View File

@@ -0,0 +1,64 @@
package com.ai.app.tools;
import com.ai.app.entity.Course;
import com.ai.app.entity.CourseReservation;
import com.ai.app.entity.School;
import com.ai.app.query.CourseQuery;
import com.ai.app.service.ICourseReservationService;
import com.ai.app.service.ICourseService;
import com.ai.app.service.ISchoolService;
import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.util.List;
@RequiredArgsConstructor
@Component
public class CourseTools {
private final ICourseService courseService;
private final ISchoolService schoolService;
private final ICourseReservationService reservationService;
@Tool(description = "根据条件查询课程")
public List<Course> queryCourse(@ToolParam(description = "查询的条件", required = false) CourseQuery query) {
if (query == null) {
return courseService.list();
}
QueryChainWrapper<Course> wrapper = courseService.query()
.eq(query.getType() != null, "type", query.getType()) // type = '编程'
.le(query.getEdu() != null, "edu", query.getEdu());// edu <= 2
if (query.getSorts() != null && !query.getSorts().isEmpty()) {
for (CourseQuery.Sort sort : query.getSorts()) {
wrapper.orderBy(true, sort.getAsc(), sort.getField());
}
}
return wrapper.list();
}
@Tool(description = "查询所有校区")
public List<School> querySchool() {
return schoolService.list();
}
@Tool(description = "生成预约单,返回预约单号")
public Integer createCourseReservation(
@ToolParam(description = "预约课程") String course,
@ToolParam(description = "预约校区") String school,
@ToolParam(description = "学生姓名") String studentName,
@ToolParam(description = "联系电话") String contactInfo,
@ToolParam(description = "备注", required = false) String remark) {
CourseReservation reservation = new CourseReservation();
reservation.setCourse(course);
reservation.setSchool(school);
reservation.setStudentName(studentName);
reservation.setContactInfo(contactInfo);
reservation.setRemark(remark);
reservationService.save(reservation);
return reservation.getId();
}
}

View File

@@ -0,0 +1,27 @@
package com.ai.app.vo;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ai.chat.messages.Message;
@NoArgsConstructor
@Data
public class MessageVO {
private String role;
private String content;
public MessageVO(Message message) {
switch (message.getMessageType()) {
case USER:
role = "user";
break;
case ASSISTANT:
role = "assistant";
break;
default:
role = "";
break;
}
this.content = message.getText();
}
}

View File

@@ -0,0 +1,24 @@
package com.ai.app.vo;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class Result {
private Integer ok;
private String msg;
private Result(Integer ok, String msg) {
this.ok = ok;
this.msg = msg;
}
public static Result ok() {
return new Result(1, "ok");
}
public static Result fail(String msg) {
return new Result(0, msg);
}
}

View File

@@ -45,7 +45,7 @@ spring:
options:
model: deepseek-reasoner
dashscope:
api-key: ${DASHSCOPE_API_KEY}
api-key: ${DASH_SCOPE_API_KEY}
chat:
options:
model: qwen-max