Some checks failed
Java Maven 3.9.9 & JDK 26 CI/CD Pipeline / build-and-deploy (push) Failing after 9m3s
222 lines
9.0 KiB
Java
222 lines
9.0 KiB
Java
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<String, Object> args = objectMapper.readValue(argumentsJson,
|
|
new TypeReference<Map<String, Object>>() {});
|
|
String sql = (String) args.get("sql");
|
|
|
|
// 校验并执行 SQL
|
|
String validatedSql = sqlValidationService.validateSelect(sql);
|
|
String queryResultJson = executeSelectSql(validatedSql);
|
|
|
|
// 构建对话历史发送回 LLM
|
|
List<DeepSeekRequest.Message> 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<Map<String, Object>> rows = new ArrayList<>();
|
|
while (rs.next()) {
|
|
java.util.LinkedHashMap<String, Object> 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<DeepSeekRequest.Message> buildToolResultMessages(
|
|
String originalText, DeepSeekResponse.ResponseMessage assistantMsg,
|
|
DeepSeekRequest.ToolCall toolCall, String queryResultJson) {
|
|
|
|
List<DeepSeekRequest.Message> 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;
|
|
}
|
|
}
|