1.7.0后端

feat(AI):完成AI自动生成报表功能;
fix(AI):完善AI问答。
master
xs 3 months ago
parent 0960d04194
commit 0103c1c901

@ -17,15 +17,22 @@ public interface HwMomAiConstants {
public static final String AI_FORM_SETTING_FIELD_TYPE_DICT = "3";//字典数据 public static final String AI_FORM_SETTING_FIELD_TYPE_DICT = "3";//字典数据
/**
* ai_chat_messagemessagge_typeai_chat_message_detailmessage_detail_type
*/
public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_QUESTION = "1";//AI问答 public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_QUESTION = "1";//AI问答
public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_KNOWLEDGE_QUESTION = "2";//AI知识库问答 public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_KNOWLEDGE_QUESTION = "2";//AI知识库问答
public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_SQL = "3";//AI生成SQL public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_SQL = "3";//AI生成SQL
public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_FORM = "4";//AI填报 public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_FORM = "4";//AI填报
public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_QUESTION_VECTOR = "5";//提问获取向量 public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_QUESTION_VECTOR = "5";//提问获取向量
public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_CONTENT_VECTOR = "6";//上传内容获取向量 public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_CONTENT_VECTOR = "6";//上传内容获取向量
public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_REPORT_SQL = "7";//AI生成报表SQL
public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_REPORT = "8";//AI生成报表SQL
public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_TEST = "9";//AI测试 public static final String AI_CHAT_MESSAGE_DETAIL_TYPE_TEST = "9";//AI测试
public static final String AI_ASR_RESULT_SUCCESS = "1";//语音识别结果:成功
public static final String AI_ASR_RESULT_FAILED = "0";//语音识别结果:失败
} }

@ -53,6 +53,17 @@
<artifactId>ruoyi-common-mybatis</artifactId> <artifactId>ruoyi-common-mybatis</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.25.0</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-excel</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.dromara</groupId> <groupId>org.dromara</groupId>
<artifactId>ruoyi-common-dubbo</artifactId> <artifactId>ruoyi-common-dubbo</artifactId>
@ -114,18 +125,7 @@
</dependency> </dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>hwmom-api-mes</artifactId>
<version>2.2.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>hwmom-api-pda</artifactId>
<version>2.2.2</version>
<scope>compile</scope>
</dependency>
<dependency> <dependency>
@ -175,16 +175,16 @@
</dependency> </dependency>
<!-- OpenAI SDK --> <!-- OpenAI SDK -->
<dependency> <!-- <dependency>-->
<groupId>com.theokanning.openai-gpt3-java</groupId> <!-- <groupId>com.theokanning.openai-gpt3-java</groupId>-->
<artifactId>service</artifactId> <!-- <artifactId>service</artifactId>-->
<version>0.18.0</version> <!-- <version>0.18.0</version>-->
</dependency> <!-- </dependency>-->
<dependency> <!-- <dependency>-->
<groupId>com.theokanning.openai-gpt3-java</groupId> <!-- <groupId>com.theokanning.openai-gpt3-java</groupId>-->
<artifactId>client</artifactId> <!-- <artifactId>client</artifactId>-->
<version>0.18.0</version> <!-- <version>0.18.0</version>-->
</dependency> <!-- </dependency>-->
<!--阿里云百炼SDK,通义千问--> <!--阿里云百炼SDK,通义千问-->
<dependency> <dependency>
@ -196,22 +196,22 @@
<!-- 本地模型依赖以sentence-transformers为例 --> <!-- 本地模型依赖以sentence-transformers为例 -->
<dependency> <!-- <dependency>-->
<groupId>org.tensorflow</groupId> <!-- <groupId>org.tensorflow</groupId>-->
<artifactId>tensorflow-core-platform</artifactId> <!-- <artifactId>tensorflow-core-platform</artifactId>-->
<version>0.5.0</version> <!-- <version>0.5.0</version>-->
</dependency> <!-- </dependency>-->
<dependency> <!-- <dependency>-->
<groupId>org.deeplearning4j</groupId> <!-- <groupId>org.deeplearning4j</groupId>-->
<artifactId>deeplearning4j-core</artifactId> <!-- <artifactId>deeplearning4j-core</artifactId>-->
<version>1.0.0-M2.1</version> <!-- <version>1.0.0-M2.1</version>-->
</dependency> <!-- </dependency>-->
<dependency> <!-- <dependency>-->
<groupId>ai.djl</groupId> <!-- <groupId>ai.djl</groupId>-->
<artifactId>api</artifactId> <!-- <artifactId>api</artifactId>-->
<version>0.28.0</version> <!-- <version>0.28.0</version>-->
</dependency> <!-- </dependency>-->
<dependency> <dependency>
<groupId>ai.djl</groupId> <groupId>ai.djl</groupId>
<artifactId>model-zoo</artifactId> <artifactId>model-zoo</artifactId>
@ -294,16 +294,16 @@
<dependency> <!-- <dependency>-->
<groupId>org.apache.poi</groupId> <!-- <groupId>org.apache.poi</groupId>-->
<artifactId>poi-ooxml</artifactId> <!-- <artifactId>poi-ooxml</artifactId>-->
<version>5.2.3</version> <!-- <version>5.2.3</version>-->
</dependency> <!-- </dependency>-->
<dependency> <!-- <dependency>-->
<groupId>commons-io</groupId> <!-- <groupId>commons-io</groupId>-->
<artifactId>commons-io</artifactId> <!-- <artifactId>commons-io</artifactId>-->
<version>2.13.0</version> <!-- <version>2.13.0</version>-->
</dependency> <!-- </dependency>-->
<dependency> <dependency>
<groupId>org.anyline</groupId> <groupId>org.anyline</groupId>

@ -90,21 +90,27 @@ public class AliAsrServiceImpl implements AliAsrService {
public String recognizeSpeechByUrl(String audioUrl) { public String recognizeSpeechByUrl(String audioUrl) {
try { try {
// 构建识别参数 // 构建识别参数
if(StringUtils.isEmpty(apiKey) || StringUtils.isEmpty(modelName)){ if (StringUtils.isEmpty(apiKey) || StringUtils.isEmpty(modelName)) {
throw new RuntimeException("请配置apiKey和modelName"); throw new RuntimeException("请配置apiKey和modelName");
} }
TranscriptionParam param = TranscriptionParam.builder() TranscriptionParam param = TranscriptionParam.builder()
.apiKey(EncryptUtils.decryptByBase64(apiKey)) .apiKey(EncryptUtils.decryptByBase64(apiKey))
.model(modelName) .model(modelName)
// .model("paraformer-v2")
.fileUrls(Arrays.asList(audioUrl)) .fileUrls(Arrays.asList(audioUrl))
.parameter("language_hints", LANGUAGE_HINTS) .parameter("language_hints", LANGUAGE_HINTS)
.build(); .build();
Transcription transcription = new Transcription(); Transcription transcription = new Transcription();
// 提交转写请求
TranscriptionResult result = transcription.asyncCall(param);
System.out.println("RequestId: " + result.getRequestId());
// 阻塞等待任务完成并获取结果
result = transcription.wait(
TranscriptionQueryParam.FromTranscriptionParam(param, result.getTaskId()));
// 提交语音识别任务 // 提交语音识别任务
TranscriptionResult result = transcription.asyncCall(param);
String taskId = result.getTaskId(); String taskId = result.getTaskId();
logger.info("语音识别任务提交成功TaskId: {}", taskId); logger.info("语音识别任务提交成功TaskId: {}", taskId);
@ -118,24 +124,24 @@ public class AliAsrServiceImpl implements AliAsrService {
aiAsrRecord.setCreateDept(LoginHelper.getDeptId()); aiAsrRecord.setCreateDept(LoginHelper.getDeptId());
aiAsrRecord.setCreateTime(new Date()); aiAsrRecord.setCreateTime(new Date());
while (true) { // while (true) {
result = transcription.fetch(TranscriptionQueryParam.FromTranscriptionParam(param, taskId)); // result = transcription.fetch(TranscriptionQueryParam.FromTranscriptionParam(param, taskId));
//
if (result.getTaskStatus() == TaskStatus.SUCCEEDED) { // if (result.getTaskStatus() == TaskStatus.SUCCEEDED) {
logger.info("语音识别任务完成TaskId: {}", taskId); // logger.info("语音识别任务完成TaskId: {}", taskId);
break; // break;
} else if (result.getTaskStatus() == TaskStatus.FAILED) { // } else if (result.getTaskStatus() == TaskStatus.FAILED) {
aiAsrRecord.setAsrFailedReason(result.getResults().toString()); // aiAsrRecord.setAsrFailedReason(result.getResults().toString());
aiAsrRecord.setTaskId(taskId); // aiAsrRecord.setTaskId(taskId);
aiAsrRecord.setAsrResult(HwMomAiConstants.AI_ASR_RESULT_FAILED); // aiAsrRecord.setAsrResult(HwMomAiConstants.AI_ASR_RESULT_FAILED);
aiAsrRecordMapper.insert(aiAsrRecord); // aiAsrRecordMapper.insert(aiAsrRecord);
logger.error("语音识别任务失败TaskId: {}, 错误信息: {}", taskId, result.getResults().toString()); // logger.error("语音识别任务失败TaskId: {}, 错误信息: {}", taskId, result.getResults().toString());
throw new RuntimeException("语音识别任务失败: " + result.getResults().toString()); // throw new RuntimeException("语音识别任务失败: " + result.getResults().toString());
} // }
//
// 等待1秒后再次查询 // // 等待1秒后再次查询
Thread.sleep(1000); // Thread.sleep(1000);
} // }
// 解析识别结果 // 解析识别结果
String recognitionResult = parseRecognitionResult(result); String recognitionResult = parseRecognitionResult(result);
@ -161,20 +167,23 @@ public class AliAsrServiceImpl implements AliAsrService {
throw new RuntimeException("未获取到识别结果"); throw new RuntimeException("未获取到识别结果");
} }
if (taskResultList != null && taskResultList.size() > 0) {
HttpURLConnection connection = null;
try {
TranscriptionTaskResult taskResult = taskResultList.get(0); TranscriptionTaskResult taskResult = taskResultList.get(0);
String transcriptionUrl = taskResult.getTranscriptionUrl(); String transcriptionUrl = taskResult.getTranscriptionUrl();
connection =
// 获取识别结果详情 (HttpURLConnection) new URL(transcriptionUrl).openConnection();
HttpURLConnection connection = (HttpURLConnection) new URL(transcriptionUrl).openConnection();
connection.setRequestMethod("GET"); connection.setRequestMethod("GET");
connection.connect(); connection.connect();
BufferedReader reader =
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { new BufferedReader(new InputStreamReader(connection.getInputStream()));
Gson gson = new GsonBuilder().setPrettyPrinting().create(); Gson gson = new GsonBuilder().setPrettyPrinting().create();
JsonElement jsonElement = gson.fromJson(reader, JsonElement.class); JsonElement jsonResult = gson.fromJson(reader, JsonObject.class);
System.out.println(gson.toJson(jsonResult));
if (jsonElement.isJsonObject()) { if (jsonResult.isJsonObject()) {
JsonObject jsonObject = jsonElement.getAsJsonObject(); JsonObject jsonObject = jsonResult.getAsJsonObject();
JsonArray transcriptsArr = jsonObject.getAsJsonArray("transcripts"); JsonArray transcriptsArr = jsonObject.getAsJsonArray("transcripts");
if (transcriptsArr != null && !transcriptsArr.isEmpty()) { if (transcriptsArr != null && !transcriptsArr.isEmpty()) {
@ -183,12 +192,45 @@ public class AliAsrServiceImpl implements AliAsrService {
return extractTextFromTags(text); return extractTextFromTags(text);
} }
} }
} catch (Exception e) {
throw new RuntimeException("解析识别结果失败"); throw new RuntimeException("语音识别失败:" + e.getMessage());
} finally { } finally {
if (connection != null) {
connection.disconnect(); connection.disconnect();
} }
} }
}
// TranscriptionTaskResult taskResult = taskResultList.get(0);
// String transcriptionUrl = taskResult.getTranscriptionUrl();
//
// // 获取识别结果详情
// HttpURLConnection connection = (HttpURLConnection) new URL(transcriptionUrl).openConnection();
// connection.setRequestMethod("GET");
// connection.connect();
//
// try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
// Gson gson = new GsonBuilder().setPrettyPrinting().create();
// JsonElement jsonElement = gson.fromJson(reader, JsonElement.class);
//
// if (jsonElement.isJsonObject()) {
// JsonObject jsonObject = jsonElement.getAsJsonObject();
// JsonArray transcriptsArr = jsonObject.getAsJsonArray("transcripts");
//
// if (transcriptsArr != null && !transcriptsArr.isEmpty()) {
// JsonObject jsonObject1 = transcriptsArr.get(0).getAsJsonObject();
// String text = jsonObject1.get("text").getAsString();
// return extractTextFromTags(text);
// }
// }
//
// throw new RuntimeException("解析识别结果失败");
// } finally {
// connection.disconnect();
// }
return null;
}
/** /**
* *

@ -25,6 +25,7 @@ import org.dromara.ai.process.provider.processor.AIProviderProcessorFactory;
import org.dromara.ai.process.provider.processor.IUnifiedAIProviderProcessor; import org.dromara.ai.process.provider.processor.IUnifiedAIProviderProcessor;
import org.dromara.ai.service.*; import org.dromara.ai.service.*;
import org.dromara.ai.vectordb.service.IVectorDBService; import org.dromara.ai.vectordb.service.IVectorDBService;
import org.dromara.common.constant.HwMomAiConstants;
import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.EditGroup; import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.idempotent.annotation.RepeatSubmit; import org.dromara.common.idempotent.annotation.RepeatSubmit;
@ -68,11 +69,11 @@ public class AiAssistantController extends BaseController {
} }
/** /**
* AI * AI(AI)
*/ */
@GetMapping("/getAiChatMessageList") @GetMapping("/getAiChatMessageList")
public R<List<AiChatMessageVo>> getAiChatMessageList(AiChatMessageBo bo) { public R<List<AiChatMessageVo>> getAiChatMessageList(AiChatMessageBo bo) {
List<AiChatMessageVo> list = aiChatMessageService.queryList(bo); List<AiChatMessageVo> list = aiChatMessageService.queryQaList(bo);
return R.ok(list); return R.ok(list);
} }
@ -125,6 +126,7 @@ public class AiAssistantController extends BaseController {
* *
*/ */
@PostMapping("/chat") @PostMapping("/chat")
@SaCheckPermission("ai:aiChatMessage:list")
public Mono<AIResponse> chat( public Mono<AIResponse> chat(
@RequestParam("provider") String provider, @RequestBody AIRequest request) { @RequestParam("provider") String provider, @RequestBody AIRequest request) {
@ -139,8 +141,10 @@ public class AiAssistantController extends BaseController {
* @param request * @param request
* @return * @return
*/ */
@SaCheckPermission("ai:aiAssistant:generateSql")
@PostMapping(value = "/generateSql") @PostMapping(value = "/generateSql")
public String generateSql(AIRequest request) { public String generateSql(AIRequest request) {
request.setMessageDetailType(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_SQL);
return aiAssistantService.generateSQL(request); return aiAssistantService.generateSQL(request);
} }

@ -0,0 +1,160 @@
package org.dromara.ai.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.alibaba.fastjson.JSONObject;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.dromara.ai.domain.AiChatMessage;
import org.dromara.ai.domain.bo.AiAsrRecordBo;
import org.dromara.ai.domain.bo.AiChatMessageDetailBo;
import org.dromara.ai.domain.bo.AiTokenUsageBo;
import org.dromara.ai.domain.dto.AIReportRequest;
import org.dromara.ai.domain.dto.AIReportResponse;
import org.dromara.ai.domain.vo.AiAsrRecordVo;
import org.dromara.ai.domain.vo.AiChatMessageDetailVo;
import org.dromara.ai.domain.vo.AiChatMessageVo;
import org.dromara.ai.domain.vo.AiTokenUsageVo;
import org.dromara.ai.process.dto.AIRequest;
import org.dromara.ai.service.IAIAssistantService;
import org.dromara.ai.service.IAIReportService;
import org.dromara.common.constant.HwMomAiConstants;
import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* AI
* 访:/ai/aiReport
*
* @author xins
* @date 2025-08-07
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/aiReport")
public class AiReportController extends BaseController {
private final Logger logger = LoggerFactory.getLogger(AiReportController.class);
private final IAIReportService aiReportService;
private final IAIAssistantService aiAssistantService;
/**
* sqlAIModelID, AIPLATFORMID
*
* @param request
* @return
*/
@PostMapping(value = "/generateReportSql")
@SaCheckPermission("ai:aiReport:generate")
public R<String> generateReportSql(AIRequest request) {
request.setMessageDetailType(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_REPORT_SQL);
return R.ok(aiAssistantService.generateSQL(request));
}
@PostMapping("/generateReport")
@SaCheckPermission("ai:aiReport:generate")
@ResponseBody
public R<AIReportResponse> generateReport(@RequestBody AIRequest request) {
String provider = "deepseek"; // 或从请求中获取
request.setMessageDetailType(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_REPORT);
Mono<AIReportResponse> aiReportResponseMono = aiReportService.generateReport(provider, request);
// System.out.println(aiReportResponseMono.block().getChartHtml());
return R.ok(aiReportResponseMono.block());
}
/**
* AI
*/
@GetMapping("/getRecentReports")
@SaCheckPermission("ai:aiReport:generate")
@ResponseBody
public TableDataInfo<AiChatMessage> getRecentReports() {
return aiReportService.getRecentReports();
}
/**
* AI
*/
@GetMapping("/getRecentReportInfo")
@SaCheckPermission("ai:aiReport:generate")
public R<AiChatMessageDetailVo> getRecentReportInfo(AiChatMessageDetailBo bo) {
AiChatMessageDetailVo aiChatMessageDetailVo = aiReportService.getRecentReportInfo(bo);
return R.ok(aiChatMessageDetailVo);
}
/**
* token使
*/
// @SaCheckPermission("ai:tokenUsage:export")
@Log(title = "AI生成报表", businessType = BusinessType.EXPORT)
@PostMapping("/exportReportData")
@SaCheckPermission("ai:aiReport:export")
public void exportReportData(AIRequest aiRequest, HttpServletResponse response) {
aiReportService.exportReportData(aiRequest,response);
}
@PostMapping(value = "/streamGenerate", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@SaCheckPermission("ai:aiReport:generate")
public Flux<String> streamGenerateReport(
@RequestParam("provider") String provider, @RequestBody AIRequest request) {
// LoginUser loginUser = LoginHelper.getLoginUser();
// IUnifiedAIProviderProcessor processor = aiProviderProcessorFactory.getProcessor(provider);
return aiReportService.streamGenerateReport(provider, request);
}
/**
* AI
*/
@PostMapping("/generateTestReport")
@SaCheckPermission("ai:aiReport:generate")
@ResponseBody
public R<AIReportResponse> generateTestReport(@RequestBody AIReportRequest request) {
try {
logger.info("收到AI报表生成请求: {}", request.getPrompt());
// 参数验证
if (request.getPrompt() == null || request.getPrompt().trim().isEmpty()) {
return R.fail("请输入分析需求描述");
}
// 生成报表
AIReportResponse response = aiReportService.generateTestReport(request);
logger.info("AI报表生成成功SQL: {}", response.getSql());
return R.ok(response);
} catch (Exception e) {
logger.error("AI报表生成失败", e);
return R.fail("报表生成失败: " + e.getMessage());
}
}
}

@ -0,0 +1,34 @@
package org.dromara.ai.domain.dto;
/**
* @Author xins
* @Date 2026/1/5 14:38
* @Description:AI
*/
import lombok.Data;
import java.util.Map;
@Data
public class AIReportRequest {
/**
*
*/
private String prompt;
/**
* ID
*/
private String dataSourceId;
/**
*
*/
private String chartType;
/**
*
*/
private Map<String, Object> parameters;
}

@ -0,0 +1,63 @@
package org.dromara.ai.domain.dto;
/**
* @Author xins
* @Date 2026/1/5 14:39
* @Description:AI
*/
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class AIReportResponse {
/**
* SQL
*/
private String sql;
/**
*
*/
private List<Map<String, Object>> data;
/**
*
*/
private List<FieldInfo> fields;
/**
* HTML
*/
private String chartHtml;
/**
* HTML
*/
private String tableHtml;
/**
*
*/
private String analysis;
/**
*
*/
private Long executionTime;
/**
*
*/
@Data
public static class FieldInfo {
private String name;
private String type;
private String comment;
private boolean isPrimary;
private boolean isNumeric;
}
}

@ -0,0 +1,21 @@
package org.dromara.ai.domain.dto;
/**
* @Author xins
* @Date 2026/1/5 14:39
* @Description:AI
*/
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class AIReportResult {
private Boolean success;
private String code;
private String message;
private AIReportResponse aiReportResponse;
}

@ -0,0 +1,17 @@
package org.dromara.ai.domain.dto;
import lombok.Data;
import org.dromara.ai.process.dto.TokenUsage;
/**
* @Author xins
* @Date 2026/1/7 10:17
* @Description:
*/
@Data
public class StreamResult {
private String type; // "chunk" 或 "complete"
private String content;
private TokenUsage tokenUsage;
private Boolean isComplete = false;
}

@ -64,4 +64,11 @@ public interface SQLServerDatabaseMetaMapper {
@Param("orderBy") String orderBy, @Param("orderBy") String orderBy,
@Param("groupBy") String groupBy); @Param("groupBy") String groupBy);
/**
* SELECT
* @param sql
*/
List<Map<String, Object>> dynamicSelectSql(@Param("sql") String sql);
} }

@ -125,4 +125,10 @@ public class AIRequest {
* *
*/ */
private Object customParams; private Object customParams;
private String messageDetailType;
private String sql;
private Long chatMessageId;
} }

@ -2,6 +2,7 @@ package org.dromara.ai.process.provider.processor;
import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse; import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse;
import org.dromara.ai.domain.dto.StreamResult;
import org.dromara.ai.process.dto.AIRequest; import org.dromara.ai.process.dto.AIRequest;
import org.dromara.ai.process.dto.AIResponse; import org.dromara.ai.process.dto.AIResponse;
import org.dromara.ai.process.dto.TokenUsage; import org.dromara.ai.process.dto.TokenUsage;
@ -103,4 +104,7 @@ public interface IUnifiedAIProviderProcessor {
Long modelId, Long knowledgeBaseId, Long knowledgeContentId, Long modelId, Long knowledgeBaseId, Long knowledgeContentId,
Long chatMessageId,String sessionId,String takeFlag,String completeFlag, Long chatMessageId,String sessionId,String takeFlag,String completeFlag,
Long userId,String tenantId,Long deptId); Long userId,String tenantId,Long deptId);
public Flux<StreamResult> chatStreamComplete(AIRequest request, LoginUser loginUser);
} }

@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.yulichang.toolkit.JoinWrappers; import com.github.yulichang.toolkit.JoinWrappers;
import com.github.yulichang.wrapper.MPJLambdaWrapper; import com.github.yulichang.wrapper.MPJLambdaWrapper;
import io.seata.common.util.StringUtils;
import org.dromara.ai.domain.AiChatMessage; import org.dromara.ai.domain.AiChatMessage;
import org.dromara.ai.domain.AiChatMessageDetail; import org.dromara.ai.domain.AiChatMessageDetail;
import org.dromara.ai.domain.AiTokenUsage; import org.dromara.ai.domain.AiTokenUsage;
@ -88,7 +89,7 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce
} }
protected void configureApiKey(AIRequest request) { protected void configureApiKey(AIRequest request) {
if(request.getModelId()!=null){ if (request.getModelId() != null) {
AiModelVo aiModelVo = aiModelMapper.selectVoById(request.getModelId()); AiModelVo aiModelVo = aiModelMapper.selectVoById(request.getModelId());
if (aiModelVo != null) { if (aiModelVo != null) {
if (aiModelVo.getApiKey() == null) { if (aiModelVo.getApiKey() == null) {
@ -101,12 +102,13 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce
/** /**
* http() * http()
*
* @param url * @param url
* @param requestBody * @param requestBody
* @param apiKey * @param apiKey
* @return AIResponse * @return AIResponse
*/ */
public abstract Mono<AIResponse> standardRequest(String url,String requestBody, String apiKey); public abstract Mono<AIResponse> standardRequest(String url, String requestBody, String apiKey);
/** /**
@ -165,14 +167,6 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce
} }
/** /**
* JSONtoken * JSONtoken
*/ */
@ -216,6 +210,7 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce
protected void saveChatMessage(AIRequest request, String fullResponse, TokenUsage tokenUsage, LoginUser loginUser) { protected void saveChatMessage(AIRequest request, String fullResponse, TokenUsage tokenUsage, LoginUser loginUser) {
try { try {
String messageType = StringUtils.isNotBlank(request.getMessageDetailType()) ? request.getMessageDetailType() : HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_QUESTION;
String sessionId = request.getSessionId(); String sessionId = request.getSessionId();
AiChatMessage aiChatMessage = aiChatMessageMapper AiChatMessage aiChatMessage = aiChatMessageMapper
.selectOne(new LambdaQueryWrapper<AiChatMessage>() .selectOne(new LambdaQueryWrapper<AiChatMessage>()
@ -225,7 +220,7 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce
aiChatMessage = new AiChatMessage(); aiChatMessage = new AiChatMessage();
aiChatMessage.setSessionId(request.getSessionId()); aiChatMessage.setSessionId(request.getSessionId());
aiChatMessage.setMessageTopic(objectMapper.writeValueAsString(request.getMessageTopic())); aiChatMessage.setMessageTopic(objectMapper.writeValueAsString(request.getMessageTopic()));
aiChatMessage.setMessageType(AIChatMessageTypeEnum.AI_CHAT.getCode()); aiChatMessage.setMessageType(messageType);
aiChatMessage.setModelId(request.getModelId()); aiChatMessage.setModelId(request.getModelId());
aiChatMessage.setKnowledgeBaseId(request.getKnowledgeBaseId()); aiChatMessage.setKnowledgeBaseId(request.getKnowledgeBaseId());
// aiChatMessage.setTotalToken(); // aiChatMessage.setTotalToken();
@ -239,10 +234,10 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce
} }
saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_QUESTION, objectMapper.writeValueAsString(request.getQuestionContent()), saveTokenUsage(messageType, objectMapper.writeValueAsString(request.getQuestionContent()),
objectMapper.writeValueAsString(fullResponse), tokenUsage, request.getModelId(), request.getKnowledgeBaseId(), null, objectMapper.writeValueAsString(fullResponse), tokenUsage, request.getModelId(), request.getKnowledgeBaseId(), null,
aiChatMessage.getChatMessageId(),request.getSessionId(),request.getCarryHistoryFlag(),"1", aiChatMessage.getChatMessageId(), request.getSessionId(), request.getCarryHistoryFlag(), "1",
loginUser.getUserId(),loginUser.getTenantId(), loginUser.getDeptId()); loginUser.getUserId(), loginUser.getTenantId(), loginUser.getDeptId());
// AiChatMessageDetail aiChatMessageDetail = new AiChatMessageDetail(); // AiChatMessageDetail aiChatMessageDetail = new AiChatMessageDetail();
@ -275,6 +270,7 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce
/** /**
* token使 * token使
*
* @param messageDetailType * @param messageDetailType
* @param questionContent * @param questionContent
* @param answerContent * @param answerContent
@ -293,11 +289,11 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce
@Override @Override
public void saveTokenUsage(String messageDetailType, String questionContent, String answerContent, TokenUsage tokenUsage, public void saveTokenUsage(String messageDetailType, String questionContent, String answerContent, TokenUsage tokenUsage,
Long modelId, Long knowledgeBaseId, Long knowledgeContentId, Long modelId, Long knowledgeBaseId, Long knowledgeContentId,
Long chatMessageId,String sessionId,String takeFlag,String completeFlag, Long chatMessageId, String sessionId, String takeFlag, String completeFlag,
Long userId,String tenantId,Long deptId) { Long userId, String tenantId, Long deptId) {
Long promptToken = tokenUsage!=null ? tokenUsage.getPromptToken():null; Long promptToken = tokenUsage != null ? tokenUsage.getPromptToken() : null;
Long completionToken = tokenUsage!=null ? tokenUsage.getCompletionToken():null; Long completionToken = tokenUsage != null ? tokenUsage.getCompletionToken() : null;
Long totalToken = tokenUsage!=null ? tokenUsage.getTotalToken():null; Long totalToken = tokenUsage != null ? tokenUsage.getTotalToken() : null;
AiChatMessageDetail aiChatMessageDetail = new AiChatMessageDetail(); AiChatMessageDetail aiChatMessageDetail = new AiChatMessageDetail();
aiChatMessageDetail.setMessageDetailType(messageDetailType); aiChatMessageDetail.setMessageDetailType(messageDetailType);
@ -350,7 +346,6 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce
} }
/** /**
* chunk * chunk
*/ */
@ -364,8 +359,13 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce
} }
// getters // getters
public String getContent() { return content; } public String getContent() {
public TokenUsage getTokenUsage() { return tokenUsage; } return content;
}
public TokenUsage getTokenUsage() {
return tokenUsage;
}
public boolean hasContent() { public boolean hasContent() {
return content != null && !content.isEmpty(); return content != null && !content.isEmpty();
@ -377,8 +377,6 @@ public abstract class BaseAIProviderProcessor implements IUnifiedAIProviderProce
} }
// /** // /**
// * 流式回复 // * 流式回复
// * @param request // * @param request

@ -11,6 +11,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse; import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse;
import org.dromara.ai.domain.AiChatMessage; import org.dromara.ai.domain.AiChatMessage;
import org.dromara.ai.domain.AiChatMessageDetail; import org.dromara.ai.domain.AiChatMessageDetail;
import org.dromara.ai.domain.dto.StreamResult;
import org.dromara.ai.domain.vo.AiModelVo; import org.dromara.ai.domain.vo.AiModelVo;
import org.dromara.ai.mapper.AiChatMessageDetailMapper; import org.dromara.ai.mapper.AiChatMessageDetailMapper;
import org.dromara.ai.mapper.AiChatMessageMapper; import org.dromara.ai.mapper.AiChatMessageMapper;
@ -53,16 +54,16 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor {
private final String deepSeekChatModel = "deepseek-chat"; private final String deepSeekChatModel = "deepseek-chat";
/** /**
* http() * http()
*
* @param url * @param url
* @param requestBody * @param requestBody
* @param apiKey * @param apiKey
* @return AIResponse * @return AIResponse
*/ */
@Override @Override
public Mono<AIResponse> standardRequest(String url,String requestBody, String apiKey) { public Mono<AIResponse> standardRequest(String url, String requestBody, String apiKey) {
return webClient.post() return webClient.post()
.uri(url) .uri(url)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey)
@ -146,7 +147,7 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor {
}) })
.doOnComplete(() -> { .doOnComplete(() -> {
// 流完成后保存到数据库 // 流完成后保存到数据库
saveChatMessage(request, fullResponseBuilder.toString(), null,loginUser); saveChatMessage(request, fullResponseBuilder.toString(), null, loginUser);
}) })
.doOnError(error -> { .doOnError(error -> {
// 错误处理 // 错误处理
@ -308,7 +309,7 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor {
// usageNode.path("total_tokens").asInt() // usageNode.path("total_tokens").asInt()
// ); // );
TokenUsage tokenUsage = new TokenUsage( usageNode.path("prompt_tokens").asLong(), TokenUsage tokenUsage = new TokenUsage(usageNode.path("prompt_tokens").asLong(),
usageNode.path("completion_tokens").asLong(), usageNode.path("completion_tokens").asLong(),
usageNode.path("total_tokens").asLong()); usageNode.path("total_tokens").asLong());
@ -331,7 +332,7 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor {
} }
@Override @Override
public List<Double> getEmbedding(GetEmbeddingResponse embeddingResponse){ public List<Double> getEmbedding(GetEmbeddingResponse embeddingResponse) {
return List.of(); return List.of();
} }
@ -346,6 +347,63 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor {
} }
@Override
public Flux<StreamResult> chatStreamComplete(AIRequest request, LoginUser loginUser) {
try {
ObjectNode rootNode = objectMapper.createObjectNode();
rootNode.put("model", deepSeekChatModel);
rootNode.set("messages", objectMapper.valueToTree(request.getMessages()));
rootNode.put("stream", true);
if (request.getTemperature() != null) {
rootNode.put("temperature", request.getTemperature());
}
String requestBody = objectMapper.writeValueAsString(rootNode);
configureApiKey(request);
// 用于收集完整响应和token信息
StringBuilder fullResponseBuilder = new StringBuilder();
TokenUsage finalTokenUsage = new TokenUsage(0L, 0L, 0L);
// 返回包装后的结果流
return executeStreamRequest(API_URL, requestBody, request.getApiKey())
.map(chunkResult -> {
// 收集内容
if (chunkResult.hasContent()) {
fullResponseBuilder.append(chunkResult.getContent());
}
// 更新token使用信息
if (chunkResult.hasTokenUsage()) {
TokenUsage usage = chunkResult.getTokenUsage();
finalTokenUsage.setPromptToken(usage.getPromptToken());
finalTokenUsage.setCompletionToken(usage.getCompletionToken());
finalTokenUsage.setTotalToken(usage.getTotalToken());
}
// 创建流式结果
StreamResult result = new StreamResult();
result.setType("chunk");
result.setContent(chunkResult.hasContent() ? chunkResult.getContent() : "");
return result;
})
.doOnComplete(() -> {
// 流完成后保存到数据库,但不发送额外内容
System.out.println(fullResponseBuilder.toString());
saveChatMessage(request, fullResponseBuilder.toString(), finalTokenUsage, loginUser);
})
.doOnError(error -> {
// 即使出错也尝试保存已收集的内容
saveChatMessage(request, fullResponseBuilder.toString(), finalTokenUsage, loginUser);
});
} catch (IOException e) {
return Flux.error(new RuntimeException("构建请求失败: " + e.getMessage()));
}
}
private String extractContentRaw(String jsonStr) { private String extractContentRaw(String jsonStr) {
// 简单的正则提取,作为最后手段 // 简单的正则提取,作为最后手段
Pattern pattern = Pattern.compile("\"content\":\"(.*?)(?<!\\\\)\""); Pattern pattern = Pattern.compile("\"content\":\"(.*?)(?<!\\\\)\"");
@ -359,9 +417,4 @@ public class DeepSeekProcessor extends BaseAIProviderProcessor {
} }
} }

@ -10,6 +10,7 @@ import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingRequest;
import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse; import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse;
import com.tencentcloudapi.lkeap.v20240522.models.Usage; import com.tencentcloudapi.lkeap.v20240522.models.Usage;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.domain.dto.StreamResult;
import org.dromara.ai.process.provider.processor.utils.ProcessorUtils; import org.dromara.ai.process.provider.processor.utils.ProcessorUtils;
import org.dromara.ai.test.vectorization.config.EmbeddingConfig; import org.dromara.ai.test.vectorization.config.EmbeddingConfig;
import org.dromara.ai.process.dto.AIRequest; import org.dromara.ai.process.dto.AIRequest;
@ -226,4 +227,9 @@ public class TencentLkeProcessor extends BaseAIProviderProcessor {
protected StreamChunkResult extractContentAndTokensFromStreamJson(String jsonChunk) throws Exception { protected StreamChunkResult extractContentAndTokensFromStreamJson(String jsonChunk) throws Exception {
return null; return null;
} }
@Override
public Flux<StreamResult> chatStreamComplete(AIRequest request, LoginUser loginUser) {
return null;
}
} }

@ -12,6 +12,7 @@ import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse;
import org.checkerframework.checker.units.qual.A; import org.checkerframework.checker.units.qual.A;
import org.dromara.ai.domain.AiChatMessage; import org.dromara.ai.domain.AiChatMessage;
import org.dromara.ai.domain.AiChatMessageDetail; import org.dromara.ai.domain.AiChatMessageDetail;
import org.dromara.ai.domain.dto.StreamResult;
import org.dromara.ai.mapper.AiChatMessageDetailMapper; import org.dromara.ai.mapper.AiChatMessageDetailMapper;
import org.dromara.ai.mapper.AiChatMessageMapper; import org.dromara.ai.mapper.AiChatMessageMapper;
import org.dromara.ai.process.dto.AIMessage; import org.dromara.ai.process.dto.AIMessage;
@ -431,4 +432,9 @@ public class TongYiQianWenProcessor extends BaseAIProviderProcessor {
protected StreamChunkResult extractContentAndTokensFromStreamJson(String jsonChunk) throws Exception { protected StreamChunkResult extractContentAndTokensFromStreamJson(String jsonChunk) throws Exception {
return null; return null;
} }
@Override
public Flux<StreamResult> chatStreamComplete(AIRequest request, LoginUser loginUser) {
return null;
}
} }

@ -1,6 +1,7 @@
package org.dromara.ai.service; package org.dromara.ai.service;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import org.dromara.ai.domain.dto.AIReportResponse;
import org.dromara.ai.process.dto.AIFillFormRequest; import org.dromara.ai.process.dto.AIFillFormRequest;
import org.dromara.ai.process.dto.AIRequest; import org.dromara.ai.process.dto.AIRequest;
import org.dromara.ai.process.dto.AIResponse; import org.dromara.ai.process.dto.AIResponse;
@ -33,4 +34,5 @@ public interface IAIAssistantService {
*/ */
public JSONObject aiFillForm(AIFillFormRequest aiFillFormRequest); public JSONObject aiFillForm(AIFillFormRequest aiFillFormRequest);
// public boolean testAIModel(String provider, AIRequest aiRequest); // public boolean testAIModel(String provider, AIRequest aiRequest);
} }

@ -0,0 +1,74 @@
package org.dromara.ai.service;
import jakarta.servlet.http.HttpServletResponse;
import org.dromara.ai.domain.AiChatMessage;
import org.dromara.ai.domain.bo.AiChatMessageDetailBo;
import org.dromara.ai.domain.dto.AIReportRequest;
import org.dromara.ai.domain.dto.AIReportResponse;
import org.dromara.ai.domain.vo.AiChatMessageDetailVo;
import org.dromara.ai.domain.vo.AiChatMessageVo;
import org.dromara.ai.process.dto.AIRequest;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* AI
*/
public interface IAIReportService {
/**
* ,AI
*
* @param provider
* @param aiRequest
* @return Flux<String>
*/
public Mono<AIReportResponse> generateReport(String provider, AIRequest aiRequest);
/**
* 5AI
* @return List<AiChatMessageVo>
*/
public TableDataInfo<AiChatMessage> getRecentReports();
/**
* sessionidmessagedetailtypeAI
* @param aiChatMessageDetailBo
* @return AiChatMessageDetailVo
*/
public AiChatMessageDetailVo getRecentReportInfo(AiChatMessageDetailBo aiChatMessageDetailBo);
/**
* AIexcel
* @param aiRequest
* @param response
*/
public void exportReportData(AIRequest aiRequest, HttpServletResponse response);
public Flux<String> streamGenerateReport(String provider, AIRequest aiRequest);
/**
*
* @param request
* @return
*/
AIReportResponse generateTestReport(AIReportRequest request);
/**
* AISQL
* @param prompt
* @return SQL
*/
String generateSQL(String prompt);
}

@ -88,4 +88,12 @@ public interface IAiChatMessageService {
*/ */
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public Boolean deleteWithValidBySessionId(String sessionId, Boolean isValid); public Boolean deleteWithValidBySessionId(String sessionId, Boolean isValid);
/**
* ()
*
* @param bo
* @return
*/
public List<AiChatMessageVo> queryQaList(AiChatMessageBo bo);
} }

@ -9,6 +9,7 @@ import com.tencentcloudapi.lkeap.v20240522.models.GetEmbeddingResponse;
import com.tencentcloudapi.lkeap.v20240522.models.Usage; import com.tencentcloudapi.lkeap.v20240522.models.Usage;
import org.dromara.ai.domain.AiFormSettingDetail; import org.dromara.ai.domain.AiFormSettingDetail;
import org.dromara.ai.domain.AiModel; import org.dromara.ai.domain.AiModel;
import org.dromara.ai.domain.dto.AIReportResponse;
import org.dromara.ai.domain.dto.AiTableConditionWrapper; import org.dromara.ai.domain.dto.AiTableConditionWrapper;
import org.dromara.ai.domain.dto.AiTableData; import org.dromara.ai.domain.dto.AiTableData;
import org.dromara.ai.domain.dto.AiTableQueryCondition; import org.dromara.ai.domain.dto.AiTableQueryCondition;
@ -22,6 +23,7 @@ import org.dromara.ai.service.IAIAssistantService;
import org.dromara.ai.vectordb.service.IVectorDBService; import org.dromara.ai.vectordb.service.IVectorDBService;
import org.dromara.common.constant.HwMomAiConstants; import org.dromara.common.constant.HwMomAiConstants;
import org.dromara.common.encrypt.utils.EncryptUtils; import org.dromara.common.encrypt.utils.EncryptUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.api.model.LoginUser; import org.dromara.system.api.model.LoginUser;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -31,6 +33,7 @@ import reactor.core.publisher.Flux;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -63,6 +66,417 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
@Autowired @Autowired
private SQLServerDatabaseMetaMapper sQLServerDatabaseMetaMapper; private SQLServerDatabaseMetaMapper sQLServerDatabaseMetaMapper;
private final String material_inventory_sql = "SELECT\n" +
"\tTOP 1000 wi.inventory_id,\n" +
"\twi.tenant_id,\n" +
"\twi.batch_code,\n" +
"\twi.material_id,\n" +
"\twi.location_code,\n" +
"\twi.material_categories,\n" +
"\twi.inventory_qty,\n" +
"\twi.update_time,\n" +
"\twi.lock_state,\n" +
"\twi.inventory_status,\n" +
"\twi.store_id,\n" +
"\twi.create_by,\n" +
"\twi.create_time,\n" +
"\twi.update_by,\n" +
"\twi.material_code,\n" +
"\twi.warehouse_id,\n" +
"\tbmi.material_name,\n" +
"\tbmi.material_spec,\n" +
"\tbmi.material_unit,\n" +
"\tbmt.matrial_type_name,\n" +
"\twpsp.store_place_name,\n" +
"\twpsp.store_place_code\n" +
"FROM\n" +
"\twms_inventory wi WITH(NOLOCK)\n" +
"LEFT JOIN base_material_info bmi WITH(NOLOCK) ON\n" +
"\tbmi.material_id = wi.material_id\n" +
"\tAND bmi.tenant_id = wi.tenant_id\n" +
"\tAND bmi.del_flag = '0'\n" +
"LEFT JOIN base_material_type bmt WITH(NOLOCK) ON\n" +
"\tbmt.matrial_type_id = bmi.material_type_id\n" +
"\tAND bmt.tenant_id = wi.tenant_id\n" +
"\tAND bmt.del_flag = '0'\n" +
"LEFT JOIN wms_psm_store_place wpsp WITH(NOLOCK) ON\n" +
"\twpsp.store_place_code = wi.location_code\n" +
"\tAND wpsp.tenant_id = wi.tenant_id\n" +
"WHERE\n" +
"\twi.inventory_status = '1'";
private String reportHtml = "<html>\n" +
"<head>\n" +
" <style>\n" +
" body {\n" +
" font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n" +
" margin: 20px;\n" +
" background-color: #f5f7fa;\n" +
" color: #333;\n" +
" }\n" +
" .container {\n" +
" max-width: 1400px;\n" +
" margin: 0 auto;\n" +
" background-color: white;\n" +
" padding: 25px;\n" +
" border-radius: 12px;\n" +
" box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\n" +
" }\n" +
" h1 {\n" +
" color: #2c3e50;\n" +
" border-bottom: 3px solid #3498db;\n" +
" padding-bottom: 10px;\n" +
" margin-bottom: 25px;\n" +
" }\n" +
" h2, h3 {\n" +
" color: #34495e;\n" +
" margin-top: 30px;\n" +
" }\n" +
" .stats-container {\n" +
" display: flex;\n" +
" flex-wrap: wrap;\n" +
" gap: 20px;\n" +
" margin-bottom: 30px;\n" +
" }\n" +
" .stat-card {\n" +
" flex: 1;\n" +
" min-width: 200px;\n" +
" background: linear-gradient(135deg, #f8f9fa, #e9ecef);\n" +
" padding: 20px;\n" +
" border-radius: 10px;\n" +
" box-shadow: 0 3px 10px rgba(0,0,0,0.05);\n" +
" border-left: 5px solid #3498db;\n" +
" }\n" +
" .stat-card h3 {\n" +
" margin-top: 0;\n" +
" font-size: 1.1em;\n" +
" color: #7f8c8d;\n" +
" }\n" +
" .stat-value {\n" +
" font-size: 2.2em;\n" +
" font-weight: bold;\n" +
" color: #2c3e50;\n" +
" }\n" +
" .stat-unit {\n" +
" font-size: 0.9em;\n" +
" color: #95a5a6;\n" +
" }\n" +
" .chart-container {\n" +
" height: 400px;\n" +
" margin: 25px 0;\n" +
" border-radius: 10px;\n" +
" border: 1px solid #e1e8ed;\n" +
" padding: 15px;\n" +
" background-color: #fff;\n" +
" }\n" +
" table {\n" +
" width: 100%;\n" +
" border-collapse: collapse;\n" +
" margin-top: 20px;\n" +
" box-shadow: 0 2px 8px rgba(0,0,0,0.05);\n" +
" border-radius: 8px;\n" +
" overflow: hidden;\n" +
" }\n" +
" th {\n" +
" background-color: #2c3e50;\n" +
" color: white;\n" +
" text-align: left;\n" +
" padding: 15px;\n" +
" font-weight: 600;\n" +
" }\n" +
" td {\n" +
" padding: 12px 15px;\n" +
" border-bottom: 1px solid #eee;\n" +
" }\n" +
" tr:nth-child(even) {\n" +
" background-color: #f9f9f9;\n" +
" }\n" +
" tr:hover {\n" +
" background-color: #f1f7fd;\n" +
" }\n" +
" .status-1 {\n" +
" color: #27ae60;\n" +
" font-weight: bold;\n" +
" }\n" +
" .status-0 {\n" +
" color: #e74c3c;\n" +
" font-weight: bold;\n" +
" }\n" +
" .top10-list {\n" +
" background: #f8f9fa;\n" +
" padding: 20px;\n" +
" border-radius: 10px;\n" +
" margin: 20px 0;\n" +
" }\n" +
" .top10-item {\n" +
" display: flex;\n" +
" justify-content: space-between;\n" +
" padding: 10px;\n" +
" border-bottom: 1px dashed #ddd;\n" +
" }\n" +
" .top10-item:last-child {\n" +
" border-bottom: none;\n" +
" }\n" +
" .footer {\n" +
" text-align: center;\n" +
" margin-top: 40px;\n" +
" color: #95a5a6;\n" +
" font-size: 0.9em;\n" +
" }\n" +
" </style>\n" +
"</head>\n" +
"<body>\n" +
" <div class=\"container\">\n" +
" <h1>\uD83D\uDCCA WMS库存分析报表</h1>\n" +
" \n" +
" <div class=\"stats-container\">\n" +
" <div class=\"stat-card\">\n" +
" <h3>物料种类数</h3>\n" +
" <div class=\"stat-value\" id=\"totalMaterials\">0</div>\n" +
" <div class=\"stat-unit\">种</div>\n" +
" </div>\n" +
" <div class=\"stat-card\">\n" +
" <h3>总库存量</h3>\n" +
" <div class=\"stat-value\" id=\"totalQuantity\">0.00</div>\n" +
" <div class=\"stat-unit\">单位</div>\n" +
" </div>\n" +
" <div class=\"stat-card\">\n" +
" <h3>平均库存量</h3>\n" +
" <div class=\"stat-value\" id=\"avgQuantity\">0.00</div>\n" +
" <div class=\"stat-unit\">单位/物料</div>\n" +
" </div>\n" +
" <div class=\"stat-card\">\n" +
" <h3>数据记录数</h3>\n" +
" <div class=\"stat-value\" id=\"totalRecords\">0</div>\n" +
" <div class=\"stat-unit\">条</div>\n" +
" </div>\n" +
" </div>\n" +
"\n" +
" <h2>物料库存分布</h2>\n" +
" <div id=\"chartDistribution\" class=\"chart-container\"></div>\n" +
"\n" +
" <h2>库存量TOP物料</h2>\n" +
" <div id=\"chartTop\" class=\"chart-container\"></div>\n" +
"\n" +
" <h2>物料分类统计</h2>\n" +
" <div id=\"chartByMaterial\" class=\"chart-container\"></div>\n" +
"\n" +
" <h2>详细库存数据</h2>\n" +
" <table id=\"detailTable\">\n" +
" <thead>\n" +
" <tr>\n" +
" <th>库存ID</th>\n" +
" <th>物料编码</th>\n" +
" <th>库位编码</th>\n" +
" <th>库存数量</th>\n" +
" <th>批次码</th>\n" +
" <th>库存状态</th>\n" +
" <th>锁定状态</th>\n" +
" <th>更新时间</th>\n" +
" </tr>\n" +
" </thead>\n" +
" <tbody>\n" +
" <!-- 数据将由JS填充 -->\n" +
" </tbody>\n" +
" </table>\n" +
"\n" +
" <div class=\"footer\">\n" +
" 报表生成时间: <span id=\"reportTime\"></span> | 数据来源: WMS系统\n" +
" </div>\n" +
" </div>\n" +
"\n" +
" <script>\n" +
" let echarts = window.parent.echarts;\n" +
" // 原始数据\n" +
" const stockData = [{\"库位编码\":\"BB-1-1-1\",\"库存状态\":\"1\",\"库存ID\":2018,\"库存数量\":\"30.000000\",\"锁定状态\":\"0\",\"批次码\":\"IN20250915002M10053001\",\"更新时间\":\"2025-09-19 16:00:00\"},{\"库位编码\":\"BB-1-1-3\",\"库存状态\":\"1\",\"库存ID\":2017,\"物料编码\":\"WMS-003\",\"库存数量\":\"10.000000\",\"锁定状态\":\"0\",\"批次码\":\"IN20250917001WMS-003001\",\"更新时间\":\"2025-09-17 18:10:00\"},{\"库位编码\":\"BB-1-1-1\",\"库存状态\":\"1\",\"库存ID\":2016,\"物料编码\":\"WMS-002\",\"库存数量\":\"120.000000\",\"锁定状态\":\"0\",\"批次码\":\"IN20250916001WMS-002001\",\"更新时间\":\"2025-09-16 18:45:00\"},{\"库位编码\":\"BB-1-1-1\",\"库存状态\":\"1\",\"库存ID\":2015,\"物料编码\":\"WMS-001\",\"库存数量\":\"15.000000\",\"锁定状态\":\"0\",\"批次码\":\"IN20250915001WMS-001001\",\"更新时间\":\"2025-09-15 16:30:00\"},{\"库位编码\":\"DJ-01\",\"库存状态\":\"1\",\"库存ID\":1014,\"物料编码\":\"WMS-003\",\"库存数量\":\"7.000000\",\"锁定状态\":\"0\",\"批次码\":\"IN20250903001WMS-003001\",\"更新时间\":\"2025-09-05 17:50:02\"},{\"库位编码\":\"BB-1-1-3\",\"库存状态\":\"1\",\"库存ID\":1012,\"物料编码\":\"WMS-001\",\"库存数量\":\"5.000000\",\"锁定状态\":\"0\",\"批次码\":\"test123\",\"更新时间\":\"2025-09-04 17:31:11\"},{\"库位编码\":\"BB-1-1-1\",\"库存状态\":\"1\",\"库存ID\":1013,\"物料编码\":\"WMS-002\",\"库存数量\":\"5.000000\",\"锁定状态\":\"0\",\"批次码\":\"IN20250903001WMS-002001002\",\"更新时间\":\"2025-09-04 10:11:40\"}];\n" +
"\n" +
" // 数据处理\n" +
" function processData(data) {\n" +
" // 确保物料编码存在\n" +
" data.forEach(item => {\n" +
" if (!item.物料编码) item.物料编码 = '未知物料';\n" +
" item.库存数量 = parseFloat(item.库存数量);\n" +
" });\n" +
"\n" +
" // 按物料编码分组统计\n" +
" const materialMap = {};\n" +
" data.forEach(item => {\n" +
" const matCode = item.物料编码;\n" +
" if (!materialMap[matCode]) {\n" +
" materialMap[matCode] = {\n" +
" code: matCode,\n" +
" total: 0,\n" +
" count: 0,\n" +
" locations: new Set()\n" +
" };\n" +
" }\n" +
" materialMap[matCode].total += item.库存数量;\n" +
" materialMap[matCode].count++;\n" +
" materialMap[matCode].locations.add(item.库位编码);\n" +
" });\n" +
"\n" +
" // 转换为数组并排序\n" +
" const materialStats = Object.values(materialMap).map(mat => ({\n" +
" ...mat,\n" +
" locations: Array.from(mat.locatures),\n" +
" avg: mat.total / mat.count\n" +
" }));\n" +
"\n" +
" // 按库存总量排序\n" +
" materialStats.sort((a, b) => b.total - a.total);\n" +
"\n" +
" return {\n" +
" rawData: data,\n" +
" materialStats: materialStats,\n" +
" totalQuantity: data.reduce((sum, item) => sum + item.库存数量, 0),\n" +
" totalMaterials: materialStats.length,\n" +
" totalRecords: data.length,\n" +
" avgQuantity: data.reduce((sum, item) => sum + item.库存数量, 0) / data.length\n" +
" };\n" +
" }\n" +
"\n" +
" // 更新统计卡片\n" +
" function updateStats(processed) {\n" +
" document.getElementById('totalMaterials').textContent = processed.totalMaterials;\n" +
" document.getElementById('totalQuantity').textContent = processed.totalQuantity.toFixed(2);\n" +
" document.getElementById('avgQuantity').textContent = processed.avgQuantity.toFixed(2);\n" +
" document.getElementById('totalRecords').textContent = processed.totalRecords;\n" +
" document.getElementById('reportTime').textContent = new Date().toLocaleString('zh-CN');\n" +
" }\n" +
"\n" +
" // 填充详细表格\n" +
" function renderDetailTable(data) {\n" +
" const tbody = document.querySelector('#detailTable tbody');\n" +
" tbody.innerHTML = '';\n" +
" \n" +
" data.forEach(item => {\n" +
" const row = document.createElement('tr');\n" +
" row.innerHTML = `\n" +
" <td>${item.库存ID}</td>\n" +
" <td>${item.物料编码 || '未知物料'}</td>\n" +
" <td>${item.库位编码}</td>\n" +
" <td>${item.库存数量.toFixed(2)}</td>\n" +
" <td>${item.批次码}</td>\n" +
" <td class=\"status-${item.库存状态}\">${item.库存状态 === '1' ? '正常' : '异常'}</td>\n" +
" <td class=\"status-${item.锁定状态}\">${item.锁定状态 === '0' ? '未锁定' : '已锁定'}</td>\n" +
" <td>${item.更新时间}</td>\n" +
" `;\n" +
" tbody.appendChild(row);\n" +
" });\n" +
" }\n" +
"\n" +
" // 初始化图表\n" +
" function initCharts(processed) {\n" +
" const { materialStats, rawData } = processed;\n" +
"\n" +
" // 图表1: 库存分布(按库位)\n" +
" const locationMap = {};\n" +
" rawData.forEach(item => {\n" +
" const loc = item.库位编码;\n" +
" locationMap[loc] = (locationMap[loc] || 0) + item.库存数量;\n" +
" });\n" +
" \n" +
" const chart1 = echarts.init(document.getElementById('chartDistribution'));\n" +
" chart1.setOption({\n" +
" title: { text: '各库位库存量分布', left: 'center' },\n" +
" tooltip: { trigger: 'item' },\n" +
" legend: { orient: 'vertical', left: 'left' },\n" +
" series: [{\n" +
" name: '库存量',\n" +
" type: 'pie',\n" +
" radius: '60%',\n" +
" data: Object.entries(locationMap).map(([name, value]) => ({\n" +
" name: `${name} (${value.toFixed(1)})`,\n" +
" value: value\n" +
" })),\n" +
" emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } }\n" +
" }]\n" +
" });\n" +
"\n" +
" // 图表2: 库存量TOP物料\n" +
" const topData = materialStats.slice(0, Math.min(10, materialStats.length));\n" +
" const chart2 = echarts.init(document.getElementById('chartTop'));\n" +
" chart2.setOption({\n" +
" title: { text: '库存量TOP物料', left: 'center' },\n" +
" tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },\n" +
" grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },\n" +
" xAxis: {\n" +
" type: 'category',\n" +
" data: topData.map(item => item.code),\n" +
" axisLabel: { rotate: 45 }\n" +
" },\n" +
" yAxis: { type: 'value', name: '库存量' },\n" +
" series: [{\n" +
" name: '库存总量',\n" +
" type: 'bar',\n" +
" data: topData.map(item => item.total),\n" +
" itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [\n" +
" { offset: 0, color: '#83bff6' },\n" +
" { offset: 0.5, color: '#188df0' },\n" +
" { offset: 1, color: '#188df0' }\n" +
" ]) }\n" +
" }]\n" +
" });\n" +
"\n" +
" // 图表3: 物料分类统计(库存量与记录数)\n" +
" const chart3 = echarts.init(document.getElementById('chartByMaterial'));\n" +
" chart3.setOption({\n" +
" title: { text: '物料库存统计', left: 'center' },\n" +
" tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },\n" +
" legend: { data: ['库存总量', '记录数'], top: '10%' },\n" +
" grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },\n" +
" xAxis: {\n" +
" type: 'category',\n" +
" data: materialStats.map(item => item.code),\n" +
" axisLabel: { rotate: 45 }\n" +
" },\n" +
" yAxis: [\n" +
" { type: 'value', name: '库存总量', position: 'left' },\n" +
" { type: 'value', name: '记录数', position: 'right' }\n" +
" ],\n" +
" series: [\n" +
" {\n" +
" name: '库存总量',\n" +
" type: 'bar',\n" +
" data: materialStats.map(item => item.total),\n" +
" yAxisIndex: 0,\n" +
" itemStyle: { color: '#3498db' }\n" +
" },\n" +
" {\n" +
" name: '记录数',\n" +
" type: 'line',\n" +
" data: materialStats.map(item => item.count),\n" +
" yAxisIndex: 1,\n" +
" symbol: 'circle',\n" +
" symbolSize: 8,\n" +
" lineStyle: { color: '#e74c3c', width: 3 },\n" +
" itemStyle: { color: '#e74c3c' }\n" +
" }\n" +
" ]\n" +
" });\n" +
" }\n" +
"\n" +
" // 页面加载完成后执行\n" +
" document.addEventListener('DOMContentLoaded', function() {\n" +
" const processed = processData(stockData);\n" +
" updateStats(processed);\n" +
" renderDetailTable(processed.rawData);\n" +
" initCharts(processed);\n" +
" \n" +
" // 响应窗口大小变化\n" +
" window.addEventListener('resize', function() {\n" +
" echarts.getInstanceByDom(document.getElementById('chartDistribution'))?.resize();\n" +
" echarts.getInstanceByDom(document.getElementById('chartTop'))?.resize();\n" +
" echarts.getInstanceByDom(document.getElementById('chartByMaterial'))?.resize();\n" +
" });\n" +
" });\n" +
" </script>\n" +
"</body>\n" +
"</html>";
/** /**
* *
* *
@ -129,7 +543,7 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
String messageContent = message.getContent().toString(); String messageContent = message.getContent().toString();
aiRequest.setText(messageContent); aiRequest.setText(messageContent);
aiRequest.setTexts(new String[]{messageContent}); aiRequest.setTexts(new String[]{messageContent});
StringBuilder sb = new StringBuilder(messageContent); // StringBuilder sb = new StringBuilder(messageContent);
Long embeddingModelId = aiRequest.getEmbeddingModelId(); Long embeddingModelId = aiRequest.getEmbeddingModelId();
AiModel aiModel = aiModelMapper.selectById(embeddingModelId); AiModel aiModel = aiModelMapper.selectById(embeddingModelId);
@ -146,11 +560,11 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
GetEmbeddingResponse embeddingResponses = tencentLkeProcessor.getEmbeddingResponses(aiRequest); GetEmbeddingResponse embeddingResponses = tencentLkeProcessor.getEmbeddingResponses(aiRequest);
//获取token使用数量然后保存 //获取token使用数量然后保存
Usage usage = embeddingResponses.getUsage(); Usage usage = embeddingResponses.getUsage();
TokenUsage tokenUsage = new TokenUsage(null,null,usage.getTotalTokens()); TokenUsage tokenUsage = new TokenUsage(null, null, usage.getTotalTokens());
tencentLkeProcessor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_QUESTION_VECTOR,"AI问答获取向量", null, tokenUsage, tencentLkeProcessor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_QUESTION_VECTOR, "AI问答获取向量", null, tokenUsage,
embeddingModelId, aiRequest.getKnowledgeBaseId(), null, embeddingModelId, aiRequest.getKnowledgeBaseId(), null,
null, null, "0", "1",LoginHelper.getUserId(),LoginHelper.getTenantId(),LoginHelper.getDeptId()); null, null, "0", "1", LoginHelper.getUserId(), LoginHelper.getTenantId(), LoginHelper.getDeptId());
List<Double> queryEmbedding = tencentLkeProcessor.getEmbedding(embeddingResponses); List<Double> queryEmbedding = tencentLkeProcessor.getEmbedding(embeddingResponses);
@ -165,16 +579,19 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
throw new RuntimeException("未找到相关知识库内容"); throw new RuntimeException("未找到相关知识库内容");
} }
sb.append("\n####,请从以下知识库内容中获取答案:"); // sb.append("\n####,请从以下知识库内容中获取答案:");
for (String content : searchResultList) { // for (String content : searchResultList) {
sb.append("\n####").append(content); // sb.append("\n####").append(content);
} // }
// sb.append("\n####,LLM是人工智能大模型。agent是一个智能助手agent的任务是回答用户的问题"); // sb.append("\n####,LLM是人工智能大模型。agent是一个智能助手agent的任务是回答用户的问题");
// sb.append(("\n\n注意回答问题时须严格根据我给你的系统上下文内容原文进行回答请不要自己发挥,回答时保持原来文本的段落层级")); // sb.append(("\n\n注意回答问题时须严格根据我给你的系统上下文内容原文进行回答请不要自己发挥,回答时保持原来文本的段落层级"));
sb.append((!searchResultList.isEmpty() ? "\n\n注意回答问题时须严格根据我给你的系统上下文内容原文进行回答请不要自己发挥,回答时保持原来文本的段落层级" : "")); // sb.append((!searchResultList.isEmpty() ? "\n\n注意回答问题时须严格根据我给你的系统上下文内容原文进行回答请不要自己发挥,回答时保持原来文本的段落层级" : ""));
message.setContent(sb.toString());
String optimizedPrompt = buildKnowledgeBasedPrompt(messageContent, searchResultList);
message.setContent(optimizedPrompt);
return aiRequest; return aiRequest;
} catch (Exception e) { } catch (Exception e) {
@ -183,6 +600,49 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
} }
} }
/**
*
*/
private String buildKnowledgeBasedPrompt(String userQuestion, List<String> searchResults) {
StringBuilder promptBuilder = new StringBuilder();
if (searchResults.isEmpty()) {
// 没有找到相关知识库内容
promptBuilder.append("用户问题:").append(userQuestion).append("\n\n");
promptBuilder.append("【重要提示】当前知识库中没有找到相关内容。\n");
promptBuilder.append("请直接回复:\"根据现有资料,无法回答此问题。\"");
return promptBuilder.toString();
}
// 构建结构化的prompt
promptBuilder.append("【指令说明】\n");
promptBuilder.append("你是一个专业的信息检索助手,需要严格根据提供的知识库内容回答问题。\n\n");
promptBuilder.append("【知识库内容】\n");
promptBuilder.append("以下是从知识库中检索到的相关信息:\n\n");
for (int i = 0; i < searchResults.size(); i++) {
promptBuilder.append("--- 知识片段 ").append(i + 1).append(" ---\n");
promptBuilder.append(searchResults.get(i)).append("\n\n");
}
promptBuilder.append("【用户问题】\n");
promptBuilder.append(userQuestion).append("\n\n");
promptBuilder.append("【回答要求】\n");
promptBuilder.append("1. 必须严格基于上述知识库内容回答\n");
promptBuilder.append("2. 如果知识库内容不足以回答问题,请明确说明\n");
promptBuilder.append("3. 保持原文的准确性和细节,不要添加任何外部知识\n");
promptBuilder.append("4. 如果知识库中有多个相关内容,请整合信息进行回答\n");
promptBuilder.append("5. 使用清晰、有条理的方式组织答案\n\n");
promptBuilder.append("请基于知识库内容回答用户问题:");
return promptBuilder.toString();
}
/** /**
* JSON * JSON
*/ */
@ -220,6 +680,7 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
@Override @Override
public String generateSQL(AIRequest aiRequest) { public String generateSQL(AIRequest aiRequest) {
String naturalLanguageQuery = aiRequest.getText(); String naturalLanguageQuery = aiRequest.getText();
// 1. 获取数据库结构 // 1. 获取数据库结构
String dataName = StringUtils.isNotBlank(aiRequest.getDataName()) ? aiRequest.getDataName() : "master"; String dataName = StringUtils.isNotBlank(aiRequest.getDataName()) ? aiRequest.getDataName() : "master";
String schemaDescription = redisTemplate.opsForValue().get(HwMomAiConstants.AI_DATABASE_SCHEMA_KEY_PREFIX + dataName); String schemaDescription = redisTemplate.opsForValue().get(HwMomAiConstants.AI_DATABASE_SCHEMA_KEY_PREFIX + dataName);
@ -228,72 +689,165 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
} }
JSONObject schemaJson = JSONObject.parseObject(schemaDescription); JSONObject schemaJson = JSONObject.parseObject(schemaDescription);
StringBuilder sb = new StringBuilder("SQL Server 数据库结构:\n\n"); String formattedSchema = formatDatabaseSchema(schemaJson);
schemaJson.entrySet().forEach(entry -> {
// sb.append(entry.getKey()).append("\n");
sb.append(entry.getValue()).append("\n\n");
});
// sb.append(schemaDescription);
// 2. 构建 AI 提示 // 2. 构建AI提示词使用优化后的模板
String prompt = String.format( String prompt = buildAIPrompt(formattedSchema, naturalLanguageQuery);
"你是一个专业的 SQL Server 数据库专家。必须基于表结构生成SQL,其中的表名和字段名必须来自表结构信息(如果以下数据库结构中没有则返回select * from):\n\n%s\n\n" +
"请将以下自然语言查询转换为优化的 SQL Server T-SQL 语句:\n" +
"---\n%s\n---\n\n" +
"要求:\n" +
"1. 只返回 SQL 语句,不要包含解释\n" +
"2. 使用 SQL Server 特有的语法(如 TOP 而不是 LIMIT\n" +
"3. 考虑性能优化\n" +
"4. 使用合适的索引提示(如果需要)\n" +
"5. 包含必要的 WITH(NOLOCK) 提示(适用于高并发环境)\n" +
"6. 使用 ANSI 标准的 JOIN 语法 \n",
sb.toString(), naturalLanguageQuery
);
// 3. 调用AI服务
IUnifiedAIProviderProcessor processor = aiProviderProcessorFactory.getProcessorByPlatformId(aiRequest.getPlatformId()); IUnifiedAIProviderProcessor processor = aiProviderProcessorFactory.getProcessorByPlatformId(aiRequest.getPlatformId());
AIMessage aiMessage = new AIMessage(); AIMessage aiMessage = new AIMessage();
aiMessage.setRole("user"); aiMessage.setRole("user");
aiMessage.setContent(prompt); aiMessage.setContent(prompt);
aiRequest.setMessages(Collections.singletonList(aiMessage)); aiRequest.setMessages(Collections.singletonList(aiMessage));
Mono<AIResponse> response = processor.chat(aiRequest); Mono<AIResponse> response = processor.chat(aiRequest);
if (Objects.requireNonNull(response.block()).isSuccess()) {
String content = response.block().getContent().toString();
//content内容需要转换通过deepseek返回的如
// ```sql
// SELECT
// dept_id,
// tenant_id,
// parent_id,
// ancestors,
// dept_name,
// dept_category,
// order_num,
// leader,
// phone,
// email,
// status,
// del_flag,
// create_dept,
// create_by,
// create_time,
// update_by,
// update_time
// FROM sys_dept WITH(NOLOCK)
// WHERE del_flag = '0'
// ORDER BY order_num, dept_id;
//```
processor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_SQL,prompt, content,response.block().getTokenUsage(),
aiRequest.getModelId(), null,null,
null,null,"0","1",LoginHelper.getUserId(),LoginHelper.getTenantId(),LoginHelper.getDeptId());
return extractSqlFromContent(content);
// 4. 处理响应
return handleAIResponse(response, aiRequest, processor, prompt);
}
/**
*
*/
private String formatDatabaseSchema(JSONObject schemaJson) {
StringBuilder sb = new StringBuilder("SQL Server 数据库结构:\n\n");
// 统计各类表便于AI分析
Map<String, List<String>> tableCategories = new HashMap<>();
schemaJson.entrySet().forEach(entry -> {
String tableName = entry.getKey();
String tableInfo = entry.getValue().toString();
sb.append(tableInfo).append("\n\n");
// 自动分类表(简化版,可根据实际需求扩展)
if (tableName.startsWith("wms_")) {
tableCategories.computeIfAbsent("仓储管理相关表", k -> new ArrayList<>()).add(tableName);
} else if (tableName.startsWith("base_")) {
tableCategories.computeIfAbsent("基础信息表", k -> new ArrayList<>()).add(tableName);
} else if (tableName.startsWith("dms_") || tableName.contains("sys_user")) {
tableCategories.computeIfAbsent("设备管理相关表", k -> new ArrayList<>()).add(tableName);
} else if (tableName.startsWith("qc_") || tableName.contains("inspection")) {
tableCategories.computeIfAbsent("质量管理相关表", k -> new ArrayList<>()).add(tableName);
} else if (tableName.startsWith("prod_") || tableName.contains("log")) {
tableCategories.computeIfAbsent("MES相关表", k -> new ArrayList<>()).add(tableName);
} else if (tableName.startsWith("sys_")) {
tableCategories.computeIfAbsent("系统管理相关表", k -> new ArrayList<>()).add(tableName);
} else { } else {
throw new RuntimeException("生成sql语句失败" + response.block().getErrorMessage()); tableCategories.computeIfAbsent("其他表", k -> new ArrayList<>()).add(tableName);
}
});
// 添加表分类信息帮助AI理解
sb.append("【表分类参考】\n");
tableCategories.forEach((category, tables) -> {
sb.append(category).append(": ").append(String.join(", ", tables)).append("\n");
});
sb.append("\n");
return sb.toString();
} }
/**
* AI
*/
private String buildAIPrompt(String schemaDescription, String naturalLanguageQuery) {
return String.format("""
SQL ServerSQL
#
1. ****
-
-
2. ****
- "库存""物料""用户"
- JOIN
-
3. ****
-
- ID
- 使
4. **SQL**
- 使SQL ServerTOPWITH(NOLOCK)
- 使ANSI JOIN
- ORDER BY create_time DESC update_time DESC
- status
- del_flagdel_flag = '0'del_flag
-
- AS
-
- JOINWHERE使.
5. ****
- WITH(NOLOCK)
- 使TOPTOP 1000
- JOIN
#
%s
#
%s
#
1.
2.
3. T-SQL
4. SQL
SQL""", schemaDescription, naturalLanguageQuery);
} }
/**
* AI
*/
private String handleAIResponse(Mono<AIResponse> response, AIRequest aiRequest,
IUnifiedAIProviderProcessor processor, String prompt) {
AIResponse aiResponse = response.block();
if (aiResponse == null) {
throw new RuntimeException("AI服务无响应");
}
if (!aiResponse.isSuccess()) {
throw new RuntimeException("生成SQL语句失败" + aiResponse.getErrorMessage());
}
String content = aiResponse.getContent().toString();
// 提取SQL语句
String sqlContent = extractSqlFromContent(content);
// 保存token使用记录
try {
processor.saveTokenUsage(
aiRequest.getMessageDetailType(),
prompt,
sqlContent,
aiResponse.getTokenUsage(),
aiRequest.getModelId(),
null,
null,
null,
aiRequest.getSessionId(),
"0",
"1",
LoginHelper.getUserId(),
LoginHelper.getTenantId(),
LoginHelper.getDeptId()
);
} catch (Exception e) {
throw new RuntimeException("保存token使用记录失败" + e.getMessage());
}
return sqlContent;
}
/** /**
@ -354,37 +908,120 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
} }
// 专门处理 Markdown SQL 代码块 /**
private static String extractSqlFromContent(String content) { * AISQL
// 情况1如果包含 ```sql ... ``` 格式 */
if (content.contains("```sql")) { private String extractSqlFromContent(String content) {
int start = content.indexOf("```sql") + 6; // 跳过 ```sql if (StringUtils.isBlank(content)) {
int end = content.lastIndexOf("```"); return "";
// return "SELECT 1 AS Error WHERE 1=0; -- 未生成SQL";
}
// 清理内容
String cleanedContent = content.trim();
// 1. 如果包含 ```sql ``` 标记,提取中间内容
if (cleanedContent.contains("```sql")) {
int start = cleanedContent.indexOf("```sql") + 6;
int end = cleanedContent.lastIndexOf("```");
if (end > start) { if (end > start) {
content = content.substring(start, end).trim(); cleanedContent = cleanedContent.substring(start, end).trim();
} }
} }
// 情况2如果只有 ``` ... ```(没有 sql 标注) // 2. 如果包含 ``` 标记(无语言标识),提取中间内容
if (content.contains("```")) { if (cleanedContent.contains("```")) {
int start = content.indexOf("```") + 3; int start = cleanedContent.indexOf("```") + 3;
int end = content.lastIndexOf("```"); int end = cleanedContent.lastIndexOf("```");
if (end > start) { if (end > start) {
content = content.substring(start, end).trim(); cleanedContent = cleanedContent.substring(start, end).trim();
} }
} }
// content = extractSqlFromText(content); // 3. 移除可能的SQL解释或说明文本
content = content.replace("\\n", "\n") // 处理转义的换行符 String[] lines = cleanedContent.split("\n");
.replace("\n", " "); StringBuilder sqlBuilder = new StringBuilder();
boolean sqlStarted = false;
content = content.trim(); for (String line : lines) {
if ((content.startsWith("\"") && content.endsWith("\"")) || String trimmedLine = line.trim();
(content.startsWith("'") && content.endsWith("'"))) {
content = content.substring(1, content.length() - 1); // 跳过空行和明显的非SQL行
if (trimmedLine.isEmpty() ||
trimmedLine.startsWith("--") ||
trimmedLine.startsWith("/*") ||
trimmedLine.startsWith("解释") ||
trimmedLine.startsWith("说明") ||
trimmedLine.startsWith("分析") ||
trimmedLine.toLowerCase().startsWith("here") ||
trimmedLine.toLowerCase().startsWith("this sql") ||
trimmedLine.toLowerCase().startsWith("the sql")) {
continue;
} }
return content.trim(); // 如果遇到SELECT/INSERT/UPDATE/DELETE开始收集
if (!sqlStarted &&
(trimmedLine.toUpperCase().startsWith("SELECT") ||
trimmedLine.toUpperCase().startsWith("WITH") ||
trimmedLine.toUpperCase().startsWith("INSERT") ||
trimmedLine.toUpperCase().startsWith("UPDATE") ||
trimmedLine.toUpperCase().startsWith("DELETE"))) {
sqlStarted = true;
}
if (sqlStarted) {
sqlBuilder.append(line).append("\n");
}
}
String finalSql = sqlBuilder.toString().trim();
// 4. 确保SQL以分号结束
if (!finalSql.endsWith(";")) {
finalSql += ";";
}
// 5. 如果是空SQL返回安全查询
if (StringUtils.isBlank(finalSql) || finalSql.equals(";")) {
return "";
// return "SELECT 1 AS NoSQLGenerated WHERE 1=0; -- 未能生成有效SQL";
}
return finalSql;
}
/**
* SQL
*/
private boolean validateGeneratedSQL(String sql) {
if (StringUtils.isBlank(sql)) {
return false;
}
// 基础验证
String upperSql = sql.toUpperCase();
// 检查是否包含潜在危险操作
if (upperSql.contains("DROP ") ||
upperSql.contains("TRUNCATE ") ||
upperSql.contains("ALTER ") ||
upperSql.contains("CREATE ") ||
upperSql.contains("EXEC ") ||
upperSql.contains("EXECUTE ") ||
upperSql.contains("SP_") ||
upperSql.contains("XP_")) {
// log.warn("生成的SQL可能包含危险操作: {}", sql.substring(0, Math.min(sql.length(), 100)));
return false;
}
// 检查是否是有效的SELECT查询如果是查询场景
if (!upperSql.contains("SELECT")) {
// log.warn("生成的SQL不是SELECT查询: {}", sql.substring(0, Math.min(sql.length(), 100)));
return false;
}
return true;
} }
@ -407,34 +1044,38 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
// } // }
String naturalLanguageQuery = aiFillFormRequest.getNaturalLanguageQuery()+"."; String naturalLanguageQuery = aiFillFormRequest.getNaturalLanguageQuery();
List<AiFormSettingDetail> aiFormSettingDetailList = aiFillFormRequest.getFormSettingDetailList(); List<AiFormSettingDetail> aiFormSettingDetailList = aiFillFormRequest.getFormSettingDetailList();
StringBuilder sb = new StringBuilder("你是一个智能表单填充助手。请根据我的要求、提供的数据库表结构信息生成一份用于直接填充Vue3前端表单的JSON数据。\n\n"); String prompt = buildAiFillFormPrompt(
naturalLanguageQuery,
sb.append("【要求】\n"); aiFormSettingDetailList
sb.append(naturalLanguageQuery).append(",\n\n"); );
// StringBuilder sb = new StringBuilder("你是一个智能表单填充助手。请根据我的要求、提供的数据库表结构信息生成一份用于直接填充Vue3前端表单的JSON数据。\n\n");
sb.append("【数据库表信息】\n"); //
sb.append(getFormattedFormSettingDetails(aiFormSettingDetailList)); // sb.append("【要求】\n");
// sb.append(naturalLanguageQuery).append("\n\n");
sb.append("\n【输出要求】\n"); //
sb.append("1. 请根据我的核心要求,智能推断并填充所有相关字段的值\n"); // sb.append("【数据库表信息】\n");
sb.append("2. 生成一个纯净的JSON对象不要任何额外的解释、文本或markdown代码块标记键是字段名值是根据我的要求推断出来的值\n"); // sb.append(getFormattedFormSettingDetails(aiFormSettingDetailList));
// sb.append("3. 请返回**纯净的JSON格式**数据不要任何额外的解释、文本或markdown代码块标记\n"); //
sb.append("3. 仅返回一个JSON对象不要返回JSON数组,不要返回任何测试数据和推测的数据\n"); // sb.append("\n【输出要求】\n");
sb.append("4. 其中的表名和字段名必须来自数据库表结构信息\n"); // sb.append("1. 请根据我的核心要求,智能推断并填充所有相关字段的值\n");
sb.append("5. 其他没有要求返回的数据返回空字符串\n"); // sb.append("2. 生成一个纯净的JSON对象不要任何额外的解释、文本或markdown代码块标记键是字段名值是根据我的要求推断出来的值\n");
//// sb.append("3. 请返回**纯净的JSON格式**数据不要任何额外的解释、文本或markdown代码块标记\n");
// sb.append("3. 仅返回一个JSON对象不要返回JSON数组,不要返回任何测试数据和推测的数据\n");
// sb.append("4. 其中的表名、字段名和条件中的字段名必须来自数据库表结构信息\n");
// sb.append("5. 其他没有要求返回的数据返回空字符串\n");
IUnifiedAIProviderProcessor processor = aiProviderProcessorFactory IUnifiedAIProviderProcessor processor = aiProviderProcessorFactory
.getProcessorByPlatformId(aiFillFormRequest.getPlatformId()); .getProcessorByPlatformId(aiFillFormRequest.getPlatformId());
AIMessage aiMessage = new AIMessage(); AIMessage aiMessage = new AIMessage();
aiMessage.setRole("user"); aiMessage.setRole("user");
aiMessage.setContent(sb.toString()); aiMessage.setContent(prompt);
Long modelId = aiFillFormRequest.getModelId(); Long modelId = aiFillFormRequest.getModelId();
AIRequest aiRequest = new AIRequest(); AIRequest aiRequest = new AIRequest();
aiRequest.setMessages(Collections.singletonList(aiMessage)); aiRequest.setMessages(Collections.singletonList(aiMessage));
aiRequest.setText(sb.toString()); aiRequest.setText(prompt);
aiRequest.setModelId(modelId); aiRequest.setModelId(modelId);
Mono<AIResponse> response = processor.chat(aiRequest); Mono<AIResponse> response = processor.chat(aiRequest);
@ -442,9 +1083,9 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
String content = response.block().getContent().toString(); String content = response.block().getContent().toString();
JSONObject contentJson = JSONObject.parseObject(content); JSONObject contentJson = JSONObject.parseObject(content);
parseRelateTable(aiFormSettingDetailList, contentJson); parseRelateTable(aiFormSettingDetailList, contentJson);
processor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_FORM,sb.toString(), content, response.block().getTokenUsage(), processor.saveTokenUsage(HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_FORM, prompt, content, response.block().getTokenUsage(),
modelId, null, null, modelId, null, null,
null, null, "0", "1",LoginHelper.getUserId(),LoginHelper.getTenantId(),LoginHelper.getDeptId()); null, null, "0", "1", LoginHelper.getUserId(), LoginHelper.getTenantId(), LoginHelper.getDeptId());
System.out.println(contentJson.toJSONString()); System.out.println(contentJson.toJSONString());
return contentJson; return contentJson;
@ -455,6 +1096,32 @@ public class AIAssistantServiceImpl implements IAIAssistantService {
} }
/**
* AI
*/
private String buildAiFillFormPrompt(String naturalLanguageQuery,
List<AiFormSettingDetail> formDetails) {
StringBuilder sb = new StringBuilder();
sb.append("你是一个智能表单填充助手。请根据以下要求填充表单:\n\n");
sb.append("【用户需求】\n")
.append(naturalLanguageQuery)
.append("\n\n");
sb.append("【表单结构】\n")
.append(getFormattedFormSettingDetails(formDetails))
.append("\n\n");
sb.append("【输出要求】\n")
.append("1. 根据需求推断并填充所有相关字段的值\n")
.append("2. 仅返回一个JSON对象不要任何额外文本\n")
.append("3. 键名必须来自表单结构中的字段名\n")
.append("4. 未指定的字段返回空字符串\n")
.append("5. 不要随意添加标点符号");
return sb.toString();
}
private void parseRelateTable(List<AiFormSettingDetail> aiFormSettingDetailList, JSONObject contentJson) { private void parseRelateTable(List<AiFormSettingDetail> aiFormSettingDetailList, JSONObject contentJson) {
for (AiFormSettingDetail aiFormSettingDetail : aiFormSettingDetailList) { for (AiFormSettingDetail aiFormSettingDetail : aiFormSettingDetailList) {
if (aiFormSettingDetail.getSettingFlag().equals("1")) { if (aiFormSettingDetail.getSettingFlag().equals("1")) {

@ -5,6 +5,7 @@ import com.github.yulichang.interfaces.MPJBaseJoin;
import org.dromara.ai.domain.AiChatMessageDetail; import org.dromara.ai.domain.AiChatMessageDetail;
import org.dromara.ai.domain.bo.AiChatMessageTopicBo; import org.dromara.ai.domain.bo.AiChatMessageTopicBo;
import org.dromara.ai.mapper.AiChatMessageDetailMapper; import org.dromara.ai.mapper.AiChatMessageDetailMapper;
import org.dromara.common.constant.HwMomAiConstants;
import org.dromara.common.core.utils.MapstructUtils; import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
@ -182,4 +183,18 @@ public class AiChatMessageServiceImpl implements IAiChatMessageService {
.eq(AiChatMessage::getSessionId, sessionId)) > 0; .eq(AiChatMessage::getSessionId, sessionId)) > 0;
} }
/**
* ()
*
* @param bo
* @return
*/
@Override
public List<AiChatMessageVo> queryQaList(AiChatMessageBo bo) {
MPJLambdaWrapper<AiChatMessage> lqw = buildQueryWrapper(bo);
lqw.ne(AiChatMessage::getMessageType, HwMomAiConstants.AI_CHAT_MESSAGE_DETAIL_TYPE_REPORT);
return baseMapper.selectVoList(lqw);
}
} }

@ -145,4 +145,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select> </select>
<select id="dynamicSelectSql" resultType="java.util.Map" statementType="STATEMENT">
${sql}
</select>
</mapper> </mapper>

Loading…
Cancel
Save