| | |
| | | throw new Error(msg || "更新 AI 会话置顶状态失败"); |
| | | }; |
| | | |
| | | export const clearAiSessionMemory = async (sessionId) => { |
| | | const res = await request.post(`ai/chat/session/memory/clear/${sessionId}`); |
| | | const { code, msg, data } = res.data; |
| | | if (code === 200) { |
| | | return data; |
| | | } |
| | | throw new Error(msg || "清空 AI 会话记忆失败"); |
| | | }; |
| | | |
| | | export const retainAiSessionLatestRound = async (sessionId) => { |
| | | const res = await request.post(`ai/chat/session/memory/retain-latest/${sessionId}`); |
| | | const { code, msg, data } = res.data; |
| | | if (code === 200) { |
| | | return data; |
| | | } |
| | | throw new Error(msg || "仅保留当前轮记忆失败"); |
| | | }; |
| | | |
| | | export const streamAiChat = async (payload, { signal, onEvent } = {}) => { |
| | | const token = getToken(); |
| | | const response = await fetch(`${PREFIX_BASE_URL}ai/chat/stream`, { |
| | |
| | | import AddCommentOutlinedIcon from "@mui/icons-material/AddCommentOutlined"; |
| | | import DeleteOutlineOutlinedIcon from "@mui/icons-material/DeleteOutlineOutlined"; |
| | | import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; |
| | | import AutoDeleteOutlinedIcon from "@mui/icons-material/AutoDeleteOutlined"; |
| | | import HistoryOutlinedIcon from "@mui/icons-material/HistoryOutlined"; |
| | | import PushPinOutlinedIcon from "@mui/icons-material/PushPinOutlined"; |
| | | import PushPinIcon from "@mui/icons-material/PushPin"; |
| | | import SearchOutlinedIcon from "@mui/icons-material/SearchOutlined"; |
| | | import { getAiRuntime, getAiSessions, pinAiSession, removeAiSession, renameAiSession, streamAiChat } from "@/api/ai/chat"; |
| | | import { clearAiSessionMemory, getAiRuntime, getAiSessions, pinAiSession, removeAiSession, renameAiSession, retainAiSessionLatestRound, streamAiChat } from "@/api/ai/chat"; |
| | | |
| | | const DEFAULT_PROMPT_CODE = "home.default"; |
| | | |
| | |
| | | promptName: runtime?.promptName || "--", |
| | | model: runtime?.model || "--", |
| | | mountedMcpCount: runtime?.mountedMcpCount ?? 0, |
| | | recentMessageCount: runtime?.recentMessageCount ?? 0, |
| | | hasSummary: !!runtime?.memorySummary, |
| | | hasFacts: !!runtime?.memoryFacts, |
| | | }; |
| | | }, [runtime]); |
| | | |
| | |
| | | await loadSessions(sessionKeyword); |
| | | } catch (error) { |
| | | const message = error.message || "重命名会话失败"; |
| | | setDrawerError(message); |
| | | notify(message, { type: "error" }); |
| | | } |
| | | }; |
| | | |
| | | const handleClearMemory = async () => { |
| | | if (streaming || !sessionId) { |
| | | return; |
| | | } |
| | | try { |
| | | await clearAiSessionMemory(sessionId); |
| | | notify("会话记忆已清空"); |
| | | await Promise.all([ |
| | | loadRuntime(sessionId), |
| | | loadSessions(sessionKeyword), |
| | | ]); |
| | | } catch (error) { |
| | | const message = error.message || "清空会话记忆失败"; |
| | | setDrawerError(message); |
| | | notify(message, { type: "error" }); |
| | | } |
| | | }; |
| | | |
| | | const handleRetainLatestRound = async () => { |
| | | if (streaming || !sessionId) { |
| | | return; |
| | | } |
| | | try { |
| | | await retainAiSessionLatestRound(sessionId); |
| | | notify("已仅保留当前轮记忆"); |
| | | await Promise.all([ |
| | | loadRuntime(sessionId), |
| | | loadSessions(sessionKeyword), |
| | | ]); |
| | | } catch (error) { |
| | | const message = error.message || "保留当前轮记忆失败"; |
| | | setDrawerError(message); |
| | | notify(message, { type: "error" }); |
| | | } |
| | |
| | | <Chip size="small" label={`Model: ${runtimeSummary.model}`} /> |
| | | <Chip size="small" label={`MCP: ${runtimeSummary.mountedMcpCount}`} /> |
| | | <Chip size="small" label={`History: ${persistedMessages.length}`} /> |
| | | <Chip size="small" label={`Recent: ${runtimeSummary.recentMessageCount}`} /> |
| | | <Chip size="small" color={runtimeSummary.hasSummary ? "success" : "default"} label={runtimeSummary.hasSummary ? "有摘要" : "无摘要"} /> |
| | | <Chip size="small" color={runtimeSummary.hasFacts ? "info" : "default"} label={runtimeSummary.hasFacts ? "有事实" : "无事实"} /> |
| | | </Stack> |
| | | <Stack direction="row" spacing={1} mt={1.5} flexWrap="wrap" useFlexGap> |
| | | {quickLinks.map((item) => ( |
| | |
| | | {item.label} |
| | | </Button> |
| | | ))} |
| | | <Button |
| | | size="small" |
| | | variant="outlined" |
| | | startIcon={<HistoryOutlinedIcon />} |
| | | onClick={handleRetainLatestRound} |
| | | disabled={!sessionId || streaming} |
| | | > |
| | | 仅保留当前轮 |
| | | </Button> |
| | | <Button |
| | | size="small" |
| | | variant="outlined" |
| | | color="warning" |
| | | startIcon={<AutoDeleteOutlinedIcon />} |
| | | onClick={handleClearMemory} |
| | | disabled={!sessionId || streaming} |
| | | > |
| | | 清空记忆 |
| | | </Button> |
| | | </Stack> |
| | | {!!runtime?.memorySummary && ( |
| | | <Alert severity="info" sx={{ mt: 1.5 }}> |
| | | <Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}> |
| | | {runtime.memorySummary} |
| | | </Typography> |
| | | </Alert> |
| | | )} |
| | | {!!runtime?.memoryFacts && ( |
| | | <Alert severity="success" sx={{ mt: 1.5 }}> |
| | | <Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}> |
| | | {runtime.memoryFacts} |
| | | </Typography> |
| | | </Alert> |
| | | )} |
| | | {loadingRuntime && ( |
| | | <Typography variant="body2" color="text.secondary" mt={1}> |
| | | 正在加载 AI 运行时信息... |
| | |
| | | public static final int DEFAULT_TIMEOUT_MS = 60000; |
| | | public static final double DEFAULT_TEMPERATURE = 0.7D; |
| | | public static final double DEFAULT_TOP_P = 1.0D; |
| | | public static final int MEMORY_RECENT_ROUNDS = 6; |
| | | public static final int MEMORY_SUMMARY_TRIGGER_MESSAGES = 12; |
| | | public static final int MEMORY_SUMMARY_MAX_LENGTH = 1200; |
| | | public static final int MEMORY_FACTS_MAX_LENGTH = 600; |
| | | } |
| | |
| | | } |
| | | |
| | | @PreAuthorize("isAuthenticated()") |
| | | @PostMapping("/ai/chat/session/memory/clear/{sessionId}") |
| | | public R clearSessionMemory(@PathVariable Long sessionId) { |
| | | aiChatService.clearSessionMemory(sessionId, getLoginUserId(), getTenantId()); |
| | | return R.ok("Clear Success").add(sessionId); |
| | | } |
| | | |
| | | @PreAuthorize("isAuthenticated()") |
| | | @PostMapping("/ai/chat/session/memory/retain-latest/{sessionId}") |
| | | public R retainLatestRound(@PathVariable Long sessionId) { |
| | | aiChatService.retainLatestRound(sessionId, getLoginUserId(), getTenantId()); |
| | | return R.ok("Retain Success").add(sessionId); |
| | | } |
| | | |
| | | @PreAuthorize("isAuthenticated()") |
| | | @PostMapping(value = "/ai/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) |
| | | public SseEmitter stream(@RequestBody AiChatRequest request) { |
| | | String requestId = StringUtils.hasText(request.getRequestId()) |
| | |
| | | |
| | | private Long sessionId; |
| | | |
| | | private String memorySummary; |
| | | |
| | | private String memoryFacts; |
| | | |
| | | private Integer recentMessageCount; |
| | | |
| | | private List<AiChatMessageDto> persistedMessages; |
| | | |
| | | private List<AiChatMessageDto> shortMemoryMessages; |
| | | } |
| | |
| | | |
| | | private List<String> mountErrors; |
| | | |
| | | private String memorySummary; |
| | | |
| | | private String memoryFacts; |
| | | |
| | | private Integer recentMessageCount; |
| | | |
| | | private List<AiChatMessageDto> persistedMessages; |
| | | } |
| | |
| | | @ApiModelProperty(value = "消息内容") |
| | | private String content; |
| | | |
| | | @ApiModelProperty(value = "内容长度") |
| | | private Integer contentLength; |
| | | |
| | | @ApiModelProperty(value = "用户 ID") |
| | | private Long userId; |
| | | |
| | |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | private Date lastMessageTime; |
| | | |
| | | @ApiModelProperty(value = "记忆摘要") |
| | | private String memorySummary; |
| | | |
| | | @ApiModelProperty(value = "关键事实") |
| | | private String memoryFacts; |
| | | |
| | | @ApiModelProperty(value = "是否置顶") |
| | | private Integer pinned; |
| | | |
| | |
| | | AiChatSessionDto renameSession(Long userId, Long tenantId, Long sessionId, AiChatSessionRenameRequest request); |
| | | |
| | | AiChatSessionDto pinSession(Long userId, Long tenantId, Long sessionId, AiChatSessionPinRequest request); |
| | | |
| | | void clearSessionMemory(Long userId, Long tenantId, Long sessionId); |
| | | |
| | | void retainLatestRound(Long userId, Long tenantId, Long sessionId); |
| | | } |
| | |
| | | AiChatSessionDto renameSession(Long sessionId, AiChatSessionRenameRequest request, Long userId, Long tenantId); |
| | | |
| | | AiChatSessionDto pinSession(Long sessionId, AiChatSessionPinRequest request, Long userId, Long tenantId); |
| | | |
| | | void clearSessionMemory(Long sessionId, Long userId, Long tenantId); |
| | | |
| | | void retainLatestRound(Long sessionId, Long userId, Long tenantId); |
| | | } |
| | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.vincent.rsf.framework.common.Cools; |
| | | import com.vincent.rsf.framework.exception.CoolException; |
| | | import com.vincent.rsf.server.ai.config.AiDefaults; |
| | | import com.vincent.rsf.server.ai.dto.AiChatMemoryDto; |
| | | import com.vincent.rsf.server.ai.dto.AiChatMessageDto; |
| | | import com.vincent.rsf.server.ai.dto.AiChatSessionPinRequest; |
| | |
| | | import java.util.ArrayList; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import java.util.Locale; |
| | | |
| | | @Service |
| | | @RequiredArgsConstructor |
| | |
| | | if (session == null) { |
| | | return AiChatMemoryDto.builder() |
| | | .sessionId(null) |
| | | .memorySummary(null) |
| | | .memoryFacts(null) |
| | | .recentMessageCount(0) |
| | | .persistedMessages(List.of()) |
| | | .shortMemoryMessages(List.of()) |
| | | .build(); |
| | | } |
| | | List<AiChatMessageDto> persistedMessages = listMessages(session.getId()); |
| | | List<AiChatMessageDto> shortMemoryMessages = tailMessagesByRounds(persistedMessages, AiDefaults.MEMORY_RECENT_ROUNDS); |
| | | return AiChatMemoryDto.builder() |
| | | .sessionId(session.getId()) |
| | | .persistedMessages(listMessages(session.getId())) |
| | | .memorySummary(session.getMemorySummary()) |
| | | .memoryFacts(session.getMemoryFacts()) |
| | | .recentMessageCount(shortMemoryMessages.size()) |
| | | .persistedMessages(persistedMessages) |
| | | .shortMemoryMessages(shortMemoryMessages) |
| | | .build(); |
| | | } |
| | | |
| | |
| | | .setUpdateBy(userId) |
| | | .setUpdateTime(now); |
| | | aiChatSessionMapper.updateById(update); |
| | | refreshMemoryProfile(session.getId(), userId); |
| | | } |
| | | |
| | | @Override |
| | |
| | | return buildSessionDto(requireOwnedSession(sessionId, userId, tenantId)); |
| | | } |
| | | |
| | | @Override |
| | | public void clearSessionMemory(Long userId, Long tenantId, Long sessionId) { |
| | | ensureIdentity(userId, tenantId); |
| | | AiChatSession session = requireOwnedSession(sessionId, userId, tenantId); |
| | | List<AiChatMessage> messages = aiChatMessageMapper.selectList(new LambdaQueryWrapper<AiChatMessage>() |
| | | .eq(AiChatMessage::getSessionId, sessionId) |
| | | .eq(AiChatMessage::getDeleted, 0)); |
| | | for (AiChatMessage message : messages) { |
| | | aiChatMessageMapper.updateById(new AiChatMessage() |
| | | .setId(message.getId()) |
| | | .setDeleted(1)); |
| | | } |
| | | aiChatSessionMapper.updateById(new AiChatSession() |
| | | .setId(sessionId) |
| | | .setMemorySummary(null) |
| | | .setMemoryFacts(null) |
| | | .setUpdateBy(userId) |
| | | .setUpdateTime(new Date()) |
| | | .setLastMessageTime(session.getCreateTime())); |
| | | } |
| | | |
| | | @Override |
| | | public void retainLatestRound(Long userId, Long tenantId, Long sessionId) { |
| | | ensureIdentity(userId, tenantId); |
| | | requireOwnedSession(sessionId, userId, tenantId); |
| | | List<AiChatMessage> records = listMessageRecords(sessionId); |
| | | if (records.isEmpty()) { |
| | | return; |
| | | } |
| | | List<AiChatMessage> retained = tailMessageRecordsByRounds(records, 1); |
| | | for (AiChatMessage message : records) { |
| | | boolean shouldKeep = retained.stream().anyMatch(item -> item.getId().equals(message.getId())); |
| | | if (!shouldKeep) { |
| | | aiChatMessageMapper.updateById(new AiChatMessage() |
| | | .setId(message.getId()) |
| | | .setDeleted(1)); |
| | | } |
| | | } |
| | | refreshMemoryProfile(sessionId, userId); |
| | | } |
| | | |
| | | private AiChatSession findLatestSession(Long userId, Long tenantId, String promptCode) { |
| | | return aiChatSessionMapper.selectOne(new LambdaQueryWrapper<AiChatSession>() |
| | | .eq(AiChatSession::getUserId, userId) |
| | |
| | | } |
| | | |
| | | private List<AiChatMessageDto> listMessages(Long sessionId) { |
| | | List<AiChatMessage> records = aiChatMessageMapper.selectList(new LambdaQueryWrapper<AiChatMessage>() |
| | | .eq(AiChatMessage::getSessionId, sessionId) |
| | | .eq(AiChatMessage::getDeleted, 0) |
| | | .orderByAsc(AiChatMessage::getSeqNo) |
| | | .orderByAsc(AiChatMessage::getId)); |
| | | List<AiChatMessage> records = listMessageRecords(sessionId); |
| | | if (Cools.isEmpty(records)) { |
| | | return List.of(); |
| | | } |
| | |
| | | return normalized; |
| | | } |
| | | |
| | | private List<AiChatMessage> listMessageRecords(Long sessionId) { |
| | | return aiChatMessageMapper.selectList(new LambdaQueryWrapper<AiChatMessage>() |
| | | .eq(AiChatMessage::getSessionId, sessionId) |
| | | .eq(AiChatMessage::getDeleted, 0) |
| | | .orderByAsc(AiChatMessage::getSeqNo) |
| | | .orderByAsc(AiChatMessage::getId)); |
| | | } |
| | | |
| | | private int findNextSeqNo(Long sessionId) { |
| | | AiChatMessage lastMessage = aiChatMessageMapper.selectOne(new LambdaQueryWrapper<AiChatMessage>() |
| | | .eq(AiChatMessage::getSessionId, sessionId) |
| | |
| | | .setSeqNo(seqNo) |
| | | .setRole(role) |
| | | .setContent(content) |
| | | .setContentLength(content == null ? 0 : content.length()) |
| | | .setUserId(userId) |
| | | .setTenantId(tenantId) |
| | | .setDeleted(0) |
| | |
| | | return normalized.length() > 80 ? normalized.substring(0, 80) : normalized; |
| | | } |
| | | |
| | | private void refreshMemoryProfile(Long sessionId, Long userId) { |
| | | List<AiChatMessageDto> messages = listMessages(sessionId); |
| | | List<AiChatMessageDto> shortMemoryMessages = tailMessagesByRounds(messages, AiDefaults.MEMORY_RECENT_ROUNDS); |
| | | List<AiChatMessageDto> historyMessages = messages.size() > shortMemoryMessages.size() |
| | | ? messages.subList(0, messages.size() - shortMemoryMessages.size()) |
| | | : List.of(); |
| | | String memorySummary = historyMessages.size() >= AiDefaults.MEMORY_SUMMARY_TRIGGER_MESSAGES |
| | | ? buildMemorySummary(historyMessages) |
| | | : null; |
| | | String memoryFacts = buildMemoryFacts(messages); |
| | | AiChatMessage lastMessage = aiChatMessageMapper.selectOne(new LambdaQueryWrapper<AiChatMessage>() |
| | | .eq(AiChatMessage::getSessionId, sessionId) |
| | | .eq(AiChatMessage::getDeleted, 0) |
| | | .orderByDesc(AiChatMessage::getSeqNo) |
| | | .orderByDesc(AiChatMessage::getId) |
| | | .last("limit 1")); |
| | | aiChatSessionMapper.updateById(new AiChatSession() |
| | | .setId(sessionId) |
| | | .setMemorySummary(memorySummary) |
| | | .setMemoryFacts(memoryFacts) |
| | | .setLastMessageTime(lastMessage == null ? null : lastMessage.getCreateTime()) |
| | | .setUpdateBy(userId) |
| | | .setUpdateTime(new Date())); |
| | | } |
| | | |
| | | private List<AiChatMessageDto> tailMessagesByRounds(List<AiChatMessageDto> source, int rounds) { |
| | | if (Cools.isEmpty(source) || rounds <= 0) { |
| | | return List.of(); |
| | | } |
| | | int userCount = 0; |
| | | int startIndex = source.size(); |
| | | for (int i = source.size() - 1; i >= 0; i--) { |
| | | AiChatMessageDto item = source.get(i); |
| | | startIndex = i; |
| | | if (item != null && "user".equalsIgnoreCase(item.getRole())) { |
| | | userCount++; |
| | | if (userCount >= rounds) { |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | return new ArrayList<>(source.subList(Math.max(0, startIndex), source.size())); |
| | | } |
| | | |
| | | private List<AiChatMessage> tailMessageRecordsByRounds(List<AiChatMessage> source, int rounds) { |
| | | if (Cools.isEmpty(source) || rounds <= 0) { |
| | | return List.of(); |
| | | } |
| | | int userCount = 0; |
| | | int startIndex = source.size(); |
| | | for (int i = source.size() - 1; i >= 0; i--) { |
| | | AiChatMessage item = source.get(i); |
| | | startIndex = i; |
| | | if (item != null && "user".equalsIgnoreCase(item.getRole())) { |
| | | userCount++; |
| | | if (userCount >= rounds) { |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | return new ArrayList<>(source.subList(Math.max(0, startIndex), source.size())); |
| | | } |
| | | |
| | | private String buildMemorySummary(List<AiChatMessageDto> historyMessages) { |
| | | StringBuilder builder = new StringBuilder("较早对话摘要:\n"); |
| | | for (AiChatMessageDto item : historyMessages) { |
| | | if (item == null || !StringUtils.hasText(item.getContent())) { |
| | | continue; |
| | | } |
| | | String prefix = "assistant".equalsIgnoreCase(item.getRole()) ? "- AI: " : "- 用户: "; |
| | | String content = compactText(item.getContent(), 120); |
| | | if (!StringUtils.hasText(content)) { |
| | | continue; |
| | | } |
| | | builder.append(prefix).append(content).append("\n"); |
| | | if (builder.length() >= AiDefaults.MEMORY_SUMMARY_MAX_LENGTH) { |
| | | break; |
| | | } |
| | | } |
| | | return compactText(builder.toString(), AiDefaults.MEMORY_SUMMARY_MAX_LENGTH); |
| | | } |
| | | |
| | | private String buildMemoryFacts(List<AiChatMessageDto> messages) { |
| | | if (Cools.isEmpty(messages)) { |
| | | return null; |
| | | } |
| | | StringBuilder builder = new StringBuilder("关键事实:\n"); |
| | | int userFacts = 0; |
| | | for (int i = messages.size() - 1; i >= 0 && userFacts < 4; i--) { |
| | | AiChatMessageDto item = messages.get(i); |
| | | if (item == null || !"user".equalsIgnoreCase(item.getRole()) || !StringUtils.hasText(item.getContent())) { |
| | | continue; |
| | | } |
| | | builder.append("- 用户关注: ").append(compactText(item.getContent(), 100)).append("\n"); |
| | | userFacts++; |
| | | } |
| | | return userFacts == 0 ? null : compactText(builder.toString(), AiDefaults.MEMORY_FACTS_MAX_LENGTH); |
| | | } |
| | | |
| | | private String compactText(String content, int maxLength) { |
| | | if (!StringUtils.hasText(content)) { |
| | | return null; |
| | | } |
| | | String normalized = content.trim() |
| | | .replace("\r", " ") |
| | | .replace("\n", " ") |
| | | .replaceAll("\\s+", " "); |
| | | return normalized.length() > maxLength ? normalized.substring(0, maxLength) : normalized; |
| | | } |
| | | |
| | | private void ensureIdentity(Long userId, Long tenantId) { |
| | | if (userId == null) { |
| | | throw new CoolException("当前登录用户不存在"); |
| | |
| | | .mountedMcpCount(config.getMcpMounts().size()) |
| | | .mountedMcpNames(config.getMcpMounts().stream().map(item -> item.getName()).toList()) |
| | | .mountErrors(List.of()) |
| | | .memorySummary(memory.getMemorySummary()) |
| | | .memoryFacts(memory.getMemoryFacts()) |
| | | .recentMessageCount(memory.getRecentMessageCount()) |
| | | .persistedMessages(memory.getPersistedMessages()) |
| | | .build(); |
| | | } |
| | |
| | | } |
| | | |
| | | @Override |
| | | public void clearSessionMemory(Long sessionId, Long userId, Long tenantId) { |
| | | aiChatMemoryService.clearSessionMemory(userId, tenantId, sessionId); |
| | | } |
| | | |
| | | @Override |
| | | public void retainLatestRound(Long sessionId, Long userId, Long tenantId) { |
| | | aiChatMemoryService.retainLatestRound(userId, tenantId, sessionId); |
| | | } |
| | | |
| | | @Override |
| | | public SseEmitter stream(AiChatRequest request, Long userId, Long tenantId) { |
| | | SseEmitter emitter = new SseEmitter(AiDefaults.SSE_TIMEOUT_MS); |
| | | CompletableFuture.runAsync(() -> doStream(request, userId, tenantId, emitter), aiChatTaskExecutor); |
| | |
| | | AiChatSession session = resolveSession(request, userId, tenantId, config.getPromptCode()); |
| | | sessionId = session.getId(); |
| | | AiChatMemoryDto memory = loadMemory(userId, tenantId, config.getPromptCode(), session.getId()); |
| | | List<AiChatMessageDto> mergedMessages = mergeMessages(memory.getPersistedMessages(), request.getMessages()); |
| | | List<AiChatMessageDto> mergedMessages = mergeMessages(memory.getShortMemoryMessages(), request.getMessages()); |
| | | try (McpMountRuntimeFactory.McpMountRuntime runtime = createRuntime(config, userId)) { |
| | | emitStrict(emitter, "start", AiChatRuntimeDto.builder() |
| | | .requestId(requestId) |
| | |
| | | .mountedMcpCount(runtime.getMountedCount()) |
| | | .mountedMcpNames(runtime.getMountedNames()) |
| | | .mountErrors(runtime.getErrors()) |
| | | .memorySummary(memory.getMemorySummary()) |
| | | .memoryFacts(memory.getMemoryFacts()) |
| | | .recentMessageCount(memory.getRecentMessageCount()) |
| | | .persistedMessages(memory.getPersistedMessages()) |
| | | .build()); |
| | | emitSafely(emitter, "status", AiChatStatusDto.builder() |
| | |
| | | requestId, userId, tenantId, session.getId(), resolvedModel); |
| | | |
| | | Prompt prompt = new Prompt( |
| | | buildPromptMessages(mergedMessages, config.getPrompt(), request.getMetadata()), |
| | | buildPromptMessages(memory, mergedMessages, config.getPrompt(), request.getMetadata()), |
| | | buildChatOptions(config.getAiParam(), runtime.getToolCallbacks(), userId, request.getMetadata()) |
| | | ); |
| | | OpenAiChatModel chatModel = createChatModel(config.getAiParam()); |
| | |
| | | return builder.build(); |
| | | } |
| | | |
| | | private List<Message> buildPromptMessages(List<AiChatMessageDto> sourceMessages, AiPrompt aiPrompt, Map<String, Object> metadata) { |
| | | private List<Message> buildPromptMessages(AiChatMemoryDto memory, List<AiChatMessageDto> sourceMessages, AiPrompt aiPrompt, Map<String, Object> metadata) { |
| | | if (Cools.isEmpty(sourceMessages)) { |
| | | throw new CoolException("对话消息不能为空"); |
| | | } |
| | |
| | | if (StringUtils.hasText(aiPrompt.getSystemPrompt())) { |
| | | messages.add(new SystemMessage(aiPrompt.getSystemPrompt())); |
| | | } |
| | | if (memory != null && StringUtils.hasText(memory.getMemorySummary())) { |
| | | messages.add(new SystemMessage("历史摘要:\n" + memory.getMemorySummary())); |
| | | } |
| | | if (memory != null && StringUtils.hasText(memory.getMemoryFacts())) { |
| | | messages.add(new SystemMessage("关键事实:\n" + memory.getMemoryFacts())); |
| | | } |
| | | int lastUserIndex = -1; |
| | | for (int i = 0; i < sourceMessages.size(); i++) { |
| | | AiChatMessageDto item = sourceMessages.get(i); |
| | |
| | | `user_id` bigint(20) NOT NULL COMMENT '用户 ID', |
| | | `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户', |
| | | `last_message_time` datetime DEFAULT NULL COMMENT '最后消息时间', |
| | | `memory_summary` longtext COMMENT '记忆摘要', |
| | | `memory_facts` text COMMENT '关键事实', |
| | | `pinned` tinyint(1) DEFAULT '0' COMMENT '是否置顶', |
| | | `status` int(11) DEFAULT '1' COMMENT '状态', |
| | | `deleted` int(11) DEFAULT '0' COMMENT '删除标记', |
| | |
| | | `seq_no` int(11) NOT NULL COMMENT '消息序号', |
| | | `role` varchar(32) NOT NULL COMMENT '消息角色', |
| | | `content` longtext COMMENT '消息内容', |
| | | `content_length` int(11) DEFAULT NULL COMMENT '内容长度', |
| | | `user_id` bigint(20) NOT NULL COMMENT '用户 ID', |
| | | `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户', |
| | | `deleted` int(11) DEFAULT '0' COMMENT '删除标记', |
| | |
| | | EXECUTE chat_session_pinned_stmt; |
| | | DEALLOCATE PREPARE chat_session_pinned_stmt; |
| | | |
| | | SET @chat_session_summary_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM `information_schema`.`COLUMNS` |
| | | WHERE `TABLE_SCHEMA` = DATABASE() |
| | | AND `TABLE_NAME` = 'sys_ai_chat_session' |
| | | AND `COLUMN_NAME` = 'memory_summary' |
| | | ); |
| | | SET @chat_session_summary_sql := IF( |
| | | @chat_session_summary_exists = 0, |
| | | 'ALTER TABLE `sys_ai_chat_session` ADD COLUMN `memory_summary` longtext COMMENT ''记忆摘要'' AFTER `last_message_time`', |
| | | 'SELECT 1' |
| | | ); |
| | | PREPARE chat_session_summary_stmt FROM @chat_session_summary_sql; |
| | | EXECUTE chat_session_summary_stmt; |
| | | DEALLOCATE PREPARE chat_session_summary_stmt; |
| | | |
| | | SET @chat_session_facts_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM `information_schema`.`COLUMNS` |
| | | WHERE `TABLE_SCHEMA` = DATABASE() |
| | | AND `TABLE_NAME` = 'sys_ai_chat_session' |
| | | AND `COLUMN_NAME` = 'memory_facts' |
| | | ); |
| | | SET @chat_session_facts_sql := IF( |
| | | @chat_session_facts_exists = 0, |
| | | 'ALTER TABLE `sys_ai_chat_session` ADD COLUMN `memory_facts` text COMMENT ''关键事实'' AFTER `memory_summary`', |
| | | 'SELECT 1' |
| | | ); |
| | | PREPARE chat_session_facts_stmt FROM @chat_session_facts_sql; |
| | | EXECUTE chat_session_facts_stmt; |
| | | DEALLOCATE PREPARE chat_session_facts_stmt; |
| | | |
| | | SET @chat_message_length_exists := ( |
| | | SELECT COUNT(1) |
| | | FROM `information_schema`.`COLUMNS` |
| | | WHERE `TABLE_SCHEMA` = DATABASE() |
| | | AND `TABLE_NAME` = 'sys_ai_chat_message' |
| | | AND `COLUMN_NAME` = 'content_length' |
| | | ); |
| | | SET @chat_message_length_sql := IF( |
| | | @chat_message_length_exists = 0, |
| | | 'ALTER TABLE `sys_ai_chat_message` ADD COLUMN `content_length` int(11) DEFAULT NULL COMMENT ''内容长度'' AFTER `content`', |
| | | 'SELECT 1' |
| | | ); |
| | | PREPARE chat_message_length_stmt FROM @chat_message_length_sql; |
| | | EXECUTE chat_message_length_stmt; |
| | | DEALLOCATE PREPARE chat_message_length_stmt; |
| | | |
| | | BEGIN; |
| | | INSERT INTO `sys_ai_prompt` |
| | | (`id`, `name`, `code`, `scene`, `system_prompt`, `user_prompt_template`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`) |