package com.l.tracecd.service; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.l.tracecd.constant.Constants; import com.l.tracecd.dto.DeepSeekRequest; import com.l.tracecd.dto.DeepSeekResponse; import com.l.tracecd.dto.VoiceResponse; import com.l.tracecd.entity.DailyRecord; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import javax.sql.DataSource; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * 语音处理编排服务 * 协调 ASR → LLM → SQL 校验/执行 → 结果返回的完整流程 */ @Service public class VoiceService { private static final Logger log = LoggerFactory.getLogger(VoiceService.class); private final AsrService asrService; private final LlmService llmService; private final SqlValidationService sqlValidationService; private final RecordService recordService; private final DistinctValueService distinctValueService; private final ObjectMapper objectMapper; private final javax.sql.DataSource dataSource; public VoiceService(AsrService asrService, LlmService llmService, SqlValidationService sqlValidationService, RecordService recordService, DistinctValueService distinctValueService, ObjectMapper objectMapper, DataSource dataSource) { this.asrService = asrService; this.llmService = llmService; this.sqlValidationService = sqlValidationService; this.recordService = recordService; this.distinctValueService = distinctValueService; this.objectMapper = objectMapper; this.dataSource = dataSource; } /** * 处理语音请求的完整流程 * * @param audioBytes 音频字节数组 * @param mimeType 音频 MIME 类型 * @return 处理结果 */ public VoiceResponse processVoice(byte[] audioBytes, String mimeType) { try { // 1. ASR 识别 log.info("开始语音处理流程,音频大小: {} bytes", audioBytes.length); String recognizedText = asrService.recognize(audioBytes, mimeType); if (recognizedText == null || recognizedText.isBlank()) { return VoiceResponse.error("未能识别到语音内容,请重试"); } // 2. 发送给 LLM 分析 DeepSeekResponse.ResponseMessage llmResponse = llmService.analyze(recognizedText); // 3. 根据 LLM 响应判断意图并处理 return handleLlmResponse(llmResponse, recognizedText); } catch (Exception e) { log.error("语音处理失败", e); return VoiceResponse.error("处理失败: " + e.getMessage()); } } /** * 根据 LLM 响应处理不同意图 */ private VoiceResponse handleLlmResponse(DeepSeekResponse.ResponseMessage response, String originalText) throws Exception { // 情况1: 有 tool_calls → 查询意图 if (response.getToolCalls() != null && !response.getToolCalls().isEmpty()) { log.info("检测到查询意图,处理 function call"); return handleQueryIntent(response, originalText); } // 情况2: 内容包含 INSERT → 录入意图 String content = response.getContent(); if (content != null && content.toUpperCase().contains("INSERT INTO")) { log.info("检测到录入意图"); return handleRecordIntent(content, originalText); } // 情况3: 普通聊天回复 log.info("检测到聊天意图"); String reply = content != null ? content : "抱歉,我没有理解您的意思"; return VoiceResponse.chatReply(reply); } /** * 处理录入意图:校验 SQL → 重试 → 入库 */ private VoiceResponse handleRecordIntent(String sql, String originalText) throws Exception { String category = null; for (int i = 0; i < Constants.SQL_MAX_RETRIES; i++) { try { String validatedSql = sqlValidationService.validateInsert(sql); DailyRecord record = recordService.insertBySql(validatedSql); distinctValueService.updateDistinctValues(record); category = record.getCategory(); log.info("录入成功: {}", category); return VoiceResponse.recordSuccess(category); } catch (SqlValidationService.SqlValidationException e) { log.warn("SQL 校验失败 (第{}次): {}", i + 1, e.getMessage()); if (i < Constants.SQL_MAX_RETRIES - 1) { DeepSeekResponse.ResponseMessage retryResponse = llmService.retryInsert(originalText, e.getMessage()); String newContent = retryResponse.getContent(); if (newContent != null) { sql = newContent; } } } } return VoiceResponse.error("SQL 生成失败,已重试 " + Constants.SQL_MAX_RETRIES + " 次"); } /** * 处理查询意图:执行 function call → 返回结果给 LLM → 获取格式化回复 */ private VoiceResponse handleQueryIntent(DeepSeekResponse.ResponseMessage response, String originalText) throws Exception { DeepSeekRequest.ToolCall toolCall = response.getToolCalls().getFirst(); String functionName = toolCall.getFunction().getName(); if (!Constants.FUNCTION_QUERY_RECORDS.equals(functionName)) { log.warn("未知的 function call: {}", functionName); return VoiceResponse.chatReply(response.getContent() != null ? response.getContent() : "抱歉,无法处理该请求"); } // 解析 SQL 参数 String argumentsJson = toolCall.getFunction().getArguments(); Map args = objectMapper.readValue(argumentsJson, new TypeReference>() {}); String sql = (String) args.get("sql"); // 校验并执行 SQL String validatedSql = sqlValidationService.validateSelect(sql); String queryResultJson = executeSelectSql(validatedSql); // 构建对话历史发送回 LLM List messages = buildToolResultMessages(originalText, response, toolCall, queryResultJson); DeepSeekResponse.ResponseMessage finalResponse = llmService.continueWithToolResult(messages); String replyContent = finalResponse.getContent(); if (replyContent == null || replyContent.isBlank()) { replyContent = "查询完成,但未能生成回复"; } return VoiceResponse.queryResult(replyContent); } /** * 执行 SELECT SQL 并返回 JSON 字符串 */ private String executeSelectSql(String sql) throws Exception { log.debug("执行函数查询 SQL: {}", sql); try (var conn = dataSource.getConnection(); var stmt = conn.createStatement(); var rs = stmt.executeQuery(sql)) { var meta = rs.getMetaData(); int colCount = meta.getColumnCount(); List> rows = new ArrayList<>(); while (rs.next()) { java.util.LinkedHashMap row = new java.util.LinkedHashMap<>(); for (int i = 1; i <= colCount; i++) { row.put(meta.getColumnLabel(i), rs.getObject(i)); } rows.add(row); } String json = objectMapper.writeValueAsString(rows); log.info("查询返回 {} 条记录", rows.size()); return json; } } /** * 构建 function call 的完整对话历史 */ private List buildToolResultMessages( String originalText, DeepSeekResponse.ResponseMessage assistantMsg, DeepSeekRequest.ToolCall toolCall, String queryResultJson) { List messages = new ArrayList<>(); // system messages.add(new DeepSeekRequest.Message("system", "你是一个日常记账助手。请根据查询结果友好地回答用户的问题。多条数据请用 markdown 表格展示,表格上方展示金额汇总。")); // user original messages.add(new DeepSeekRequest.Message("user", originalText)); // assistant with tool_call DeepSeekRequest.Message assistantMessage = new DeepSeekRequest.Message("assistant", null); assistantMessage.setToolCalls(List.of(toolCall)); messages.add(assistantMessage); // tool result DeepSeekRequest.Message toolMessage = new DeepSeekRequest.Message("tool", queryResultJson); toolMessage.setToolCallId(toolCall.getId()); toolMessage.setName(Constants.FUNCTION_QUERY_RECORDS); messages.add(toolMessage); return messages; } }