|
|
@@ -1,17 +1,20 @@
|
|
1
|
1
|
package com.ruoyi.llm.service.impl;
|
|
2
|
2
|
|
|
3
|
3
|
import com.alibaba.fastjson2.JSONObject;
|
|
|
4
|
+import com.alibaba.fastjson2.JSONArray;
|
|
4
|
5
|
import com.ruoyi.common.config.RuoYiConfig;
|
|
5
|
6
|
import com.ruoyi.common.utils.DateUtils;
|
|
6
|
7
|
import com.ruoyi.common.utils.SecurityUtils;
|
|
7
|
8
|
import com.ruoyi.common.utils.SnowFlake;
|
|
8
|
9
|
import com.ruoyi.llm.domain.CmcAgent;
|
|
9
|
10
|
import com.ruoyi.llm.domain.CmcChat;
|
|
|
11
|
+import com.ruoyi.llm.domain.CmcTopic;
|
|
10
|
12
|
import com.ruoyi.llm.domain.CmcDocument;
|
|
11
|
13
|
import com.ruoyi.llm.mapper.CmcAgentMapper;
|
|
12
|
14
|
import com.ruoyi.llm.mapper.CmcChatMapper;
|
|
13
|
15
|
import com.ruoyi.llm.mapper.CmcDocumentMapper;
|
|
14
|
16
|
import com.ruoyi.llm.service.ICmcAgentService;
|
|
|
17
|
+import com.ruoyi.llm.service.ICmcTopicService;
|
|
15
|
18
|
import dev.langchain4j.data.document.Document;
|
|
16
|
19
|
import dev.langchain4j.data.document.parser.TextDocumentParser;
|
|
17
|
20
|
import dev.langchain4j.data.document.parser.apache.pdfbox.ApachePdfBoxDocumentParser;
|
|
|
@@ -41,10 +44,8 @@ import org.noear.solon.ai.chat.ChatModel;
|
|
41
|
44
|
import org.noear.solon.ai.chat.ChatResponse;
|
|
42
|
45
|
import org.noear.solon.ai.chat.ChatSession;
|
|
43
|
46
|
import org.noear.solon.ai.chat.message.ChatMessage;
|
|
|
47
|
+import org.noear.solon.ai.chat.prompt.Prompt;
|
|
44
|
48
|
import org.noear.solon.ai.chat.session.InMemoryChatSession;
|
|
45
|
|
-import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDecimalNumber;
|
|
46
|
|
-import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;
|
|
47
|
|
-import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPr;
|
|
48
|
49
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPrGeneral;
|
|
49
|
50
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
50
|
51
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
@@ -78,6 +79,9 @@ public class CmcAgentServiceImpl implements ICmcAgentService
|
|
78
|
79
|
@Autowired
|
|
79
|
80
|
private CmcChatMapper cmcChatMapper;
|
|
80
|
81
|
|
|
|
82
|
+ @Autowired
|
|
|
83
|
+ private ICmcTopicService cmcTopicService;
|
|
|
84
|
+
|
|
81
|
85
|
private String processValue = "";
|
|
82
|
86
|
|
|
83
|
87
|
private static final EmbeddingModel embeddingModel = new BgeSmallZhV15EmbeddingModel();
|
|
|
@@ -95,10 +99,10 @@ public class CmcAgentServiceImpl implements ICmcAgentService
|
|
95
|
99
|
if (milvusServiceUrl == null || milvusServiceUrl.isEmpty()) {
|
|
96
|
100
|
throw new IllegalArgumentException("milvusServiceUrl 配置不能为空");
|
|
97
|
101
|
}
|
|
98
|
|
-// milvusClient = new MilvusClientV2(
|
|
99
|
|
-// ConnectConfig.builder()
|
|
100
|
|
-// .uri(milvusServiceUrl)
|
|
101
|
|
-// .build());
|
|
|
102
|
+ milvusClient = new MilvusClientV2(
|
|
|
103
|
+ ConnectConfig.builder()
|
|
|
104
|
+ .uri(milvusServiceUrl)
|
|
|
105
|
+ .build());
|
|
102
|
106
|
}
|
|
103
|
107
|
|
|
104
|
108
|
@PreDestroy
|
|
|
@@ -189,7 +193,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService
|
|
189
|
193
|
cmcChat.setInput("招标文件地址:" + prefixPath + "/" + file.getOriginalFilename());
|
|
190
|
194
|
cmcChat.setUserId(SecurityUtils.getUserId());
|
|
191
|
195
|
cmcChatMapper.insertCmcChat(cmcChat);
|
|
192
|
|
- if (agentName.contains("技术")) {
|
|
|
196
|
+ if (agentName.contains("技术标书")) {
|
|
193
|
197
|
String[] filenameSplit = file.getOriginalFilename().split("\\.");
|
|
194
|
198
|
String outputFilename = prefixPath + "/" + file.getOriginalFilename()
|
|
195
|
199
|
.replace(filenameSplit[filenameSplit.length - 2], filenameSplit[filenameSplit.length - 2] + "_" + agentName);
|
|
|
@@ -197,36 +201,65 @@ public class CmcAgentServiceImpl implements ICmcAgentService
|
|
197
|
201
|
outputFilename = outputFilename.replace(".doc", ".docx");
|
|
198
|
202
|
if (file.getOriginalFilename().endsWith(".pdf"))
|
|
199
|
203
|
outputFilename = outputFilename.replace(".pdf", ".docx");
|
|
200
|
|
- Path outputFilePath = Paths.get(RuoYiConfig.getProfile() + outputFilename);
|
|
201
|
|
- Files.deleteIfExists(outputFilePath);
|
|
202
|
|
- InputStream fileInputStream = new FileInputStream(RuoYiConfig.getProfile() + "/upload/agent/template/technical.docx");
|
|
203
|
|
- try (XWPFDocument doc = new XWPFDocument(fileInputStream)) {
|
|
204
|
|
- // 保存文档到本地文件系统
|
|
205
|
|
- try (FileOutputStream out = new FileOutputStream(RuoYiConfig.getProfile() + outputFilename)) {
|
|
206
|
|
- doc.write(out);
|
|
207
|
|
- }
|
|
208
|
|
- }
|
|
209
|
|
- String question = "工作大纲/工作范围/招标范围/服务范围";
|
|
210
|
|
- String chapters = generateAnswerWithDocument(profilePath + "/" + file.getOriginalFilename(),
|
|
211
|
|
- RuoYiConfig.getProfile() + outputFilename, question);
|
|
212
|
|
-
|
|
213
|
|
- // 生成详细目录结构(包含二三级标题)
|
|
214
|
|
- String detailedDirectory = generateDetailedDirectory(profilePath + "/" + file.getOriginalFilename());
|
|
215
|
|
-
|
|
|
204
|
+
|
|
|
205
|
+ // 分割文档
|
|
|
206
|
+ List<TextSegment> segments = splitDocument(transferFile, 300, 50);
|
|
|
207
|
+
|
|
216
|
208
|
// 分析项目概况
|
|
217
|
|
- String projectOverview = analyzeProjectOverview(profilePath + "/" + file.getOriginalFilename());
|
|
|
209
|
+ String projectOverview = analyzeProjectOverview(profilePath + "/" + file.getOriginalFilename(), segments);
|
|
|
210
|
+ jsonObject.put("projectOverview", projectOverview);
|
|
218
|
211
|
|
|
219
|
212
|
// 分析评分要求
|
|
220
|
|
- String scoringRequirements = analyzeScoringRequirements(profilePath + "/" + file.getOriginalFilename());
|
|
|
213
|
+ String scoringRequirements = analyzeScoringRequirements(profilePath + "/" + file.getOriginalFilename(), segments, agentName);
|
|
|
214
|
+ jsonObject.put("scoringRequirements", scoringRequirements);
|
|
|
215
|
+
|
|
|
216
|
+ // 生成详细目录结构(包含二三级标题)并写入文件
|
|
|
217
|
+ JSONArray detailedDirectoryTree = generateDetailedDirectory(file.getOriginalFilename(), agentName);
|
|
|
218
|
+ String detailedDirectory = buildDirectoryText(detailedDirectoryTree);
|
|
|
219
|
+ jsonObject.put("detailedDirectory", detailedDirectoryTree);
|
|
|
220
|
+ jsonObject.put("directoryText", detailedDirectory);
|
|
221
|
221
|
|
|
222
|
|
- message = "好的,我已经收到您上传的招标文件。以下为根据招标文件生成的章节大纲:\n\n"+ chapters + "\n\n" +
|
|
|
222
|
+ message = "好的,我已经收到您上传的招标文件。以下为根据招标文件生成的章节大纲:\n\n"+ detailedDirectory + "\n\n" +
|
|
223
|
223
|
"若您对章节标题有异议,请打开" + "【<a href='/profile" + outputFilename + "'> 技术文件 " + "</a>】" + "进行修改,后续将根据修改后的章节标题,帮您生成对应章节内容。\n\n" +
|
|
224
|
224
|
"思考时间可能较长,请耐心等待!\n";
|
|
225
|
225
|
|
|
226
|
|
- // 将分析结果添加到返回对象中
|
|
227
|
|
- jsonObject.put("detailedDirectory", detailedDirectory);
|
|
228
|
|
- jsonObject.put("projectOverview", projectOverview);
|
|
229
|
|
- jsonObject.put("scoringRequirements", scoringRequirements);
|
|
|
226
|
+ // 返回输出文件名,以便前端在保存目录时使用
|
|
|
227
|
+ jsonObject.put("filename", file.getOriginalFilename()
|
|
|
228
|
+ .replace(filenameSplit[filenameSplit.length - 2], filenameSplit[filenameSplit.length - 2] + "_" + agentName));
|
|
|
229
|
+
|
|
|
230
|
+ // 根据 agentName 查询 agentId
|
|
|
231
|
+ CmcAgent queryAgent = new CmcAgent();
|
|
|
232
|
+ queryAgent.setAgentName(agentName);
|
|
|
233
|
+ List<CmcAgent> agentList = cmcAgentMapper.selectCmcAgentList(queryAgent);
|
|
|
234
|
+ Integer agentId = null;
|
|
|
235
|
+ if (!agentList.isEmpty()) {
|
|
|
236
|
+ agentId = agentList.get(0).getAgentId();
|
|
|
237
|
+ }
|
|
|
238
|
+
|
|
|
239
|
+ // 去除文件名后缀作为 topic 名称
|
|
|
240
|
+ String originalFilename = file.getOriginalFilename();
|
|
|
241
|
+ String topic = originalFilename.substring(0, originalFilename.lastIndexOf('.'));
|
|
|
242
|
+
|
|
|
243
|
+ // 创建 topic
|
|
|
244
|
+ CmcTopic cmcTopic = new CmcTopic();
|
|
|
245
|
+ cmcTopic.setTopicId(new SnowFlake().generateId());
|
|
|
246
|
+ cmcTopic.setAgentId(agentId);
|
|
|
247
|
+ cmcTopic.setTopic(topic);
|
|
|
248
|
+ cmcTopicService.insertCmcTopic(cmcTopic);
|
|
|
249
|
+ String topicId = cmcTopic.getTopicId();
|
|
|
250
|
+
|
|
|
251
|
+ // 合并 projectOverview、scoringRequirements、detailedDirectory 作为 output
|
|
|
252
|
+ StringBuilder output = new StringBuilder();
|
|
|
253
|
+ output.append("【项目概况】\n").append(projectOverview).append("\n\n");
|
|
|
254
|
+ output.append("【评分要求】\n").append(scoringRequirements).append("\n\n");
|
|
|
255
|
+ output.append("【详细目录】\n").append(detailedDirectory);
|
|
|
256
|
+
|
|
|
257
|
+ // 更新 cmc_chat 表
|
|
|
258
|
+ cmcChat.setTopicId(topicId);
|
|
|
259
|
+ cmcChat.setOutput(output.toString());
|
|
|
260
|
+ cmcChat.setOutputTime(new Date());
|
|
|
261
|
+ cmcChatMapper.updateCmcChat(cmcChat);
|
|
|
262
|
+ jsonObject.put("topicId", topicId);
|
|
230
|
263
|
}
|
|
231
|
264
|
else if (agentName.contains("检查")) {
|
|
232
|
265
|
message = generateAnswerWithDocumentContent(profilePath + "/" + file.getOriginalFilename());
|
|
|
@@ -472,7 +505,6 @@ public class CmcAgentServiceImpl implements ICmcAgentService
|
|
472
|
505
|
EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
|
|
473
|
506
|
.queryEmbedding(queryEmbedding)
|
|
474
|
507
|
.minScore(0.7) // 降低阈值以获取更多相关内容
|
|
475
|
|
- .maxResults(5) // 限制结果数量
|
|
476
|
508
|
.build();
|
|
477
|
509
|
|
|
478
|
510
|
List<EmbeddingMatch<TextSegment>> results = embeddingStore.search(embeddingSearchRequest).matches();
|
|
|
@@ -585,18 +617,20 @@ public class CmcAgentServiceImpl implements ICmcAgentService
|
|
585
|
617
|
}
|
|
586
|
618
|
|
|
587
|
619
|
/**
|
|
588
|
|
- * 调用LLM生成回答
|
|
|
620
|
+ * 调用LLM生成目录结构
|
|
589
|
621
|
*/
|
|
590
|
|
- public String generateAnswerWithDocument(String uploadFilePath, String templatePath, String question) throws IOException {
|
|
|
622
|
+ public JSONArray generateDetailedDirectory(String fileName, String agentName) throws IOException {
|
|
|
623
|
+ String prefixPath = "/upload/agent/" + agentName;
|
|
591
|
624
|
StringBuilder sb = new StringBuilder("招标文件内容:\n\n");
|
|
592
|
|
- File profilePath = new File(uploadFilePath);
|
|
|
625
|
+ File profilePath = new File(new File( RuoYiConfig.getProfile() + prefixPath) + "/" + fileName);
|
|
593
|
626
|
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
|
|
594
|
627
|
List<TextSegment> segments = splitDocument(profilePath, 300, 50);
|
|
595
|
628
|
List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
|
|
596
|
629
|
embeddingStore.addAll(embeddings, segments);
|
|
597
|
|
- Embedding queryEmbedding = embeddingModel.embed(question).content();
|
|
|
630
|
+ Embedding queryEmbedding = embeddingModel.embed("工作大纲/工作范围/招标范围/服务范围").content();
|
|
598
|
631
|
EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
|
|
599
|
632
|
.queryEmbedding(queryEmbedding)
|
|
|
633
|
+ .maxResults(5)
|
|
600
|
634
|
.minScore(0.7)
|
|
601
|
635
|
.build();
|
|
602
|
636
|
List<EmbeddingMatch<TextSegment>> results = embeddingStore.search(embeddingSearchRequest).matches();
|
|
|
@@ -607,7 +641,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService
|
|
607
|
641
|
}
|
|
608
|
642
|
//根据招标文件工作大纲要求生成二级标题
|
|
609
|
643
|
sb.append("请基于上述招标文件中提到的")
|
|
610
|
|
- .append(question)
|
|
|
644
|
+ .append("工作大纲/工作范围/招标范围/服务范围")
|
|
611
|
645
|
.append(",先列出二级章节标题,严格按以下格式,仅输出标题列表:\n")
|
|
612
|
646
|
.append("6.1 XX\n" +
|
|
613
|
647
|
"6.2 XX\n" +
|
|
|
@@ -615,117 +649,118 @@ public class CmcAgentServiceImpl implements ICmcAgentService
|
|
615
|
649
|
"......\n" +
|
|
616
|
650
|
"6.n-1 XX\n" +
|
|
617
|
651
|
"6.n XX\n");
|
|
618
|
|
- return writeChapters(sb.toString(), templatePath);
|
|
|
652
|
+ String directoryContent = writeChapters(sb.toString(), fileName, agentName);
|
|
|
653
|
+ return parseDirectoryToTree(directoryContent);
|
|
619
|
654
|
}
|
|
620
|
655
|
|
|
621
|
656
|
/**
|
|
622
|
|
- * 生成详细目录结构(包含二三级标题)
|
|
|
657
|
+ * 将目录文本解析为树结构
|
|
623
|
658
|
*/
|
|
624
|
|
- public String generateDetailedDirectory(String uploadFilePath) throws IOException {
|
|
625
|
|
- processValue = "生成详细目录结构中: 0%";
|
|
626
|
|
- StringBuilder sb = new StringBuilder("招标文件内容:\n\n");
|
|
627
|
|
- File profilePath = new File(uploadFilePath);
|
|
628
|
|
- InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
|
|
629
|
|
- List<TextSegment> segments = splitDocument(profilePath, 300, 50);
|
|
630
|
|
- List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
|
|
631
|
|
- embeddingStore.addAll(embeddings, segments);
|
|
|
659
|
+ private JSONArray parseDirectoryToTree(String directoryContent) {
|
|
|
660
|
+ JSONArray tree = new JSONArray();
|
|
|
661
|
+ Map<String, JSONObject> level1Map = new LinkedHashMap<>();
|
|
|
662
|
+ Map<String, JSONObject> level2Map = new LinkedHashMap<>();
|
|
|
663
|
+ Map<String, JSONObject> level3Map = new LinkedHashMap<>();
|
|
|
664
|
+
|
|
|
665
|
+ String[] lines = directoryContent.split("\n");
|
|
|
666
|
+ for (String line : lines) {
|
|
|
667
|
+ line = line.trim().replace("*", "").replace("#", "").replace("-", "");
|
|
|
668
|
+ if (!line.contains("6.") || line.isEmpty()) {
|
|
|
669
|
+ continue;
|
|
|
670
|
+ }
|
|
632
|
671
|
|
|
633
|
|
- // 搜索与目录相关的内容
|
|
634
|
|
- String directoryQuery = "目录结构、章节划分、文件结构";
|
|
635
|
|
- Embedding queryEmbedding = embeddingModel.embed(directoryQuery).content();
|
|
636
|
|
- EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
|
|
637
|
|
- .queryEmbedding(queryEmbedding)
|
|
638
|
|
- .minScore(0.7)
|
|
639
|
|
- .build();
|
|
640
|
|
- List<EmbeddingMatch<TextSegment>> results = embeddingStore.search(embeddingSearchRequest).matches();
|
|
641
|
|
- results.sort(Comparator.comparingDouble(EmbeddingMatch<TextSegment>::score).reversed());
|
|
|
672
|
+ String[] parts = line.split(" ", 2);
|
|
|
673
|
+ if (parts.length < 2) {
|
|
|
674
|
+ continue;
|
|
|
675
|
+ }
|
|
642
|
676
|
|
|
643
|
|
- for (EmbeddingMatch<TextSegment> embeddingMatch : results) {
|
|
644
|
|
- String content = embeddingMatch.embedded().toString();
|
|
645
|
|
- sb.append(content).append("\n\n");
|
|
|
677
|
+ String number = parts[0].trim();
|
|
|
678
|
+ String title = parts[1].trim();
|
|
|
679
|
+ int dotCount = number.split("\\.").length - 1;
|
|
|
680
|
+
|
|
|
681
|
+ if (dotCount == 1) {
|
|
|
682
|
+ JSONObject level1 = new JSONObject();
|
|
|
683
|
+ level1.put("title", number + " " + title);
|
|
|
684
|
+ level1.put("children", new JSONArray());
|
|
|
685
|
+ level1Map.put(number, level1);
|
|
|
686
|
+ } else if (dotCount == 2) {
|
|
|
687
|
+ JSONObject level2 = new JSONObject();
|
|
|
688
|
+ level2.put("title", number + " " + title);
|
|
|
689
|
+ level2.put("children", new JSONArray());
|
|
|
690
|
+ level2Map.put(number, level2);
|
|
|
691
|
+
|
|
|
692
|
+ String parentNumber = number.substring(0, number.lastIndexOf("."));
|
|
|
693
|
+ if (level1Map.containsKey(parentNumber)) {
|
|
|
694
|
+ level1Map.get(parentNumber).getJSONArray("children").add(level2);
|
|
|
695
|
+ }
|
|
|
696
|
+ } else if (dotCount == 3) {
|
|
|
697
|
+ JSONObject level3 = new JSONObject();
|
|
|
698
|
+ level3.put("title", number + " " + title);
|
|
|
699
|
+ level3Map.put(number, level3);
|
|
|
700
|
+
|
|
|
701
|
+ String parentNumber = number.substring(0, number.lastIndexOf("."));
|
|
|
702
|
+ if (level2Map.containsKey(parentNumber)) {
|
|
|
703
|
+ level2Map.get(parentNumber).getJSONArray("children").add(level3);
|
|
|
704
|
+ }
|
|
|
705
|
+ }
|
|
|
706
|
+ }
|
|
|
707
|
+
|
|
|
708
|
+ for (Map.Entry<String, JSONObject> entry : level1Map.entrySet()) {
|
|
|
709
|
+ tree.add(entry.getValue());
|
|
646
|
710
|
}
|
|
647
|
711
|
|
|
648
|
|
- // 生成详细目录结构
|
|
649
|
|
- sb.append("请基于上述招标文件内容,生成详细的目录结构,包含一级、二级和三级标题,严格按以下格式输出:\n")
|
|
650
|
|
- .append("1. 一级标题\n")
|
|
651
|
|
- .append(" 1.1 二级标题\n")
|
|
652
|
|
- .append(" 1.1.1 三级标题\n")
|
|
653
|
|
- .append(" 1.1.2 三级标题\n")
|
|
654
|
|
- .append(" 1.2 二级标题\n")
|
|
655
|
|
- .append("2. 一级标题\n")
|
|
656
|
|
- .append(" 2.1 二级标题\n")
|
|
657
|
|
- .append(" 2.1.1 三级标题\n")
|
|
658
|
|
- .append("......\n");
|
|
659
|
|
-
|
|
660
|
|
- processValue = "生成详细目录结构中: 50%";
|
|
661
|
|
- String directory = generateAnswer(sb.toString());
|
|
662
|
|
- processValue = "生成详细目录结构中: 100%";
|
|
663
|
|
- return directory;
|
|
|
712
|
+ return tree;
|
|
664
|
713
|
}
|
|
665
|
714
|
|
|
666
|
715
|
/**
|
|
667
|
|
- * 分析项目概况
|
|
|
716
|
+ * 将目录树结构转换为文本格式
|
|
668
|
717
|
*/
|
|
669
|
|
- public String analyzeProjectOverview(String uploadFilePath) throws IOException {
|
|
670
|
|
- processValue = "分析项目概况中: 0%";
|
|
671
|
|
- StringBuilder sb = new StringBuilder("招标文件内容:\n\n");
|
|
672
|
|
- File profilePath = new File(uploadFilePath);
|
|
673
|
|
- InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
|
|
674
|
|
- List<TextSegment> segments = splitDocument(profilePath, 300, 50);
|
|
675
|
|
- List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
|
|
676
|
|
- embeddingStore.addAll(embeddings, segments);
|
|
677
|
|
-
|
|
678
|
|
- // 搜索与项目概况相关的内容
|
|
679
|
|
- String overviewQuery = "项目概况、项目基本信息、项目背景、项目目标、项目范围";
|
|
680
|
|
- Embedding queryEmbedding = embeddingModel.embed(overviewQuery).content();
|
|
681
|
|
- EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
|
|
682
|
|
- .queryEmbedding(queryEmbedding)
|
|
683
|
|
- .minScore(0.7)
|
|
684
|
|
- .build();
|
|
685
|
|
- List<EmbeddingMatch<TextSegment>> results = embeddingStore.search(embeddingSearchRequest).matches();
|
|
686
|
|
- results.sort(Comparator.comparingDouble(EmbeddingMatch<TextSegment>::score).reversed());
|
|
687
|
|
-
|
|
688
|
|
- for (EmbeddingMatch<TextSegment> embeddingMatch : results) {
|
|
689
|
|
- String content = embeddingMatch.embedded().toString();
|
|
690
|
|
- sb.append(content).append("\n\n");
|
|
|
718
|
+ private String buildDirectoryText(JSONArray tree) {
|
|
|
719
|
+ StringBuilder sb = new StringBuilder();
|
|
|
720
|
+ for (int i = 0; i < tree.size(); i++) {
|
|
|
721
|
+ JSONObject level1 = tree.getJSONObject(i);
|
|
|
722
|
+ sb.append(level1.getString("title")).append("\n");
|
|
|
723
|
+ if (level1.containsKey("children")) {
|
|
|
724
|
+ JSONArray children = level1.getJSONArray("children");
|
|
|
725
|
+ for (int j = 0; j < children.size(); j++) {
|
|
|
726
|
+ JSONObject level2 = children.getJSONObject(j);
|
|
|
727
|
+ sb.append(level2.getString("title")).append("\n");
|
|
|
728
|
+ if (level2.containsKey("children")) {
|
|
|
729
|
+ JSONArray grandchildren = level2.getJSONArray("children");
|
|
|
730
|
+ for (int k = 0; k < grandchildren.size(); k++) {
|
|
|
731
|
+ JSONObject level3 = grandchildren.getJSONObject(k);
|
|
|
732
|
+ sb.append(level3.getString("title")).append("\n");
|
|
|
733
|
+ }
|
|
|
734
|
+ }
|
|
|
735
|
+ }
|
|
|
736
|
+ }
|
|
691
|
737
|
}
|
|
692
|
|
-
|
|
693
|
|
- // 分析项目概况
|
|
694
|
|
- sb.append("请基于上述招标文件内容,分析项目概况,包括但不限于以下内容:\n")
|
|
695
|
|
- .append("1. 项目名称\n")
|
|
696
|
|
- .append("2. 项目类型\n")
|
|
697
|
|
- .append("3. 项目预算\n")
|
|
698
|
|
- .append("4. 项目周期\n")
|
|
699
|
|
- .append("5. 项目地点\n")
|
|
700
|
|
- .append("6. 招标人\n")
|
|
701
|
|
- .append("7. 项目背景\n")
|
|
702
|
|
- .append("8. 项目目标\n")
|
|
703
|
|
- .append("9. 项目范围\n")
|
|
704
|
|
- .append("请以清晰的结构输出分析结果。\n");
|
|
705
|
|
-
|
|
706
|
|
- processValue = "分析项目概况中: 50%";
|
|
707
|
|
- String overview = generateAnswer(sb.toString());
|
|
708
|
|
- processValue = "分析项目概况中: 100%";
|
|
709
|
|
- return overview;
|
|
|
738
|
+ return sb.toString();
|
|
710
|
739
|
}
|
|
711
|
740
|
|
|
712
|
741
|
/**
|
|
713
|
|
- * 分析评分要求
|
|
|
742
|
+ * 分析招标文件内容(公共方法)
|
|
|
743
|
+ * @param uploadFilePath 文件路径
|
|
|
744
|
+ * @param query 查询关键词
|
|
|
745
|
+ * @param analysisPrompt 分析提示语
|
|
|
746
|
+ * @param progressStartMsg 进度开始消息
|
|
|
747
|
+ * @param progressFoundMsg 进度找到消息
|
|
|
748
|
+ * @param progressEndMsg 进度结束消息
|
|
|
749
|
+ * @return 分析结果
|
|
714
|
750
|
*/
|
|
715
|
|
- public String analyzeScoringRequirements(String uploadFilePath) throws IOException {
|
|
716
|
|
- processValue = "分析评分要求中: 0%";
|
|
|
751
|
+ private String analyzeDocumentContent(List<TextSegment> segments, String query, String analysisPrompt,
|
|
|
752
|
+ String progressStartMsg, String progressFoundMsg, String progressEndMsg) throws IOException {
|
|
|
753
|
+ processValue = progressStartMsg;
|
|
717
|
754
|
StringBuilder sb = new StringBuilder("招标文件内容:\n\n");
|
|
718
|
|
- File profilePath = new File(uploadFilePath);
|
|
719
|
755
|
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
|
|
720
|
|
- List<TextSegment> segments = splitDocument(profilePath, 300, 50);
|
|
721
|
756
|
List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
|
|
722
|
757
|
embeddingStore.addAll(embeddings, segments);
|
|
723
|
758
|
|
|
724
|
|
- // 搜索与评分要求相关的内容
|
|
725
|
|
- String scoringQuery = "评分要求、评分标准、评标办法、打分规则";
|
|
726
|
|
- Embedding queryEmbedding = embeddingModel.embed(scoringQuery).content();
|
|
|
759
|
+ // 搜索相关内容
|
|
|
760
|
+ Embedding queryEmbedding = embeddingModel.embed(query).content();
|
|
727
|
761
|
EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
|
|
728
|
762
|
.queryEmbedding(queryEmbedding)
|
|
|
763
|
+ .maxResults(5)
|
|
729
|
764
|
.minScore(0.7)
|
|
730
|
765
|
.build();
|
|
731
|
766
|
List<EmbeddingMatch<TextSegment>> results = embeddingStore.search(embeddingSearchRequest).matches();
|
|
|
@@ -736,26 +771,50 @@ public class CmcAgentServiceImpl implements ICmcAgentService
|
|
736
|
771
|
sb.append(content).append("\n\n");
|
|
737
|
772
|
}
|
|
738
|
773
|
|
|
739
|
|
- // 分析评分要求
|
|
740
|
|
- sb.append("请基于上述招标文件内容,分析评分要求,包括但不限于以下内容:\n")
|
|
741
|
|
- .append("1. 评分项及权重\n")
|
|
742
|
|
- .append("2. 技术方案评分标准\n")
|
|
743
|
|
- .append("3. 商务方案评分标准\n")
|
|
744
|
|
- .append("4. 服务方案评分标准\n")
|
|
745
|
|
- .append("5. 其他评分项\n")
|
|
746
|
|
- .append("请以清晰的结构输出分析结果,包括各项的具体分值和评分细则。\n");
|
|
747
|
|
-
|
|
748
|
|
- processValue = "分析评分要求中: 50%";
|
|
749
|
|
- String scoring = generateAnswer(sb.toString());
|
|
750
|
|
- processValue = "分析评分要求中: 100%";
|
|
751
|
|
- return scoring;
|
|
|
774
|
+ // 添加分析提示
|
|
|
775
|
+ sb.append(analysisPrompt);
|
|
|
776
|
+
|
|
|
777
|
+ processValue = progressFoundMsg;
|
|
|
778
|
+ String result = generateAnswer(sb.toString());
|
|
|
779
|
+ processValue = progressEndMsg;
|
|
|
780
|
+ return result;
|
|
|
781
|
+ }
|
|
|
782
|
+
|
|
|
783
|
+ /**
|
|
|
784
|
+ * 分析项目概况
|
|
|
785
|
+ */
|
|
|
786
|
+ public String analyzeProjectOverview(String uploadFilePath, List<TextSegment> segments) throws IOException {
|
|
|
787
|
+ String query = uploadFilePath.split("/")[uploadFilePath.split("/").length - 1] + "项目概况";
|
|
|
788
|
+ String analysisPrompt = "请基于上述招标文件内容,分析项目概况。\n" +
|
|
|
789
|
+ "请以清晰的结构输出分析结果。\n";
|
|
|
790
|
+ return analyzeDocumentContent(segments, query, analysisPrompt,
|
|
|
791
|
+ "分析项目概况中: 0%",
|
|
|
792
|
+ "已查到项目概况: 50%",
|
|
|
793
|
+ "分析项目概况中: 100%");
|
|
|
794
|
+ }
|
|
|
795
|
+
|
|
|
796
|
+ /**
|
|
|
797
|
+ * 分析评分要求
|
|
|
798
|
+ */
|
|
|
799
|
+ public String analyzeScoringRequirements(String uploadFilePath, List<TextSegment> segments, String agentName) throws IOException {
|
|
|
800
|
+ String query = "评分要求、评分标准、评标办法、打分规则";
|
|
|
801
|
+ if (agentName.contains("技术"))
|
|
|
802
|
+ query = "技术部分评分要求、评分标准、评标办法、打分规则";
|
|
|
803
|
+ else if (agentName.contains("商务"))
|
|
|
804
|
+ query = "商务部分评分要求、评分标准、评标办法、打分规则";
|
|
|
805
|
+ String analysisPrompt = "请基于上述招标文件内容,分析评分要求。\n" +
|
|
|
806
|
+ "请以清晰的结构输出分析结果,包括各项的具体分值和评分细则。\n";
|
|
|
807
|
+ return analyzeDocumentContent(segments, query, analysisPrompt,
|
|
|
808
|
+ "分析评分要求中: 0%",
|
|
|
809
|
+ "已查到评分要求: 50%",
|
|
|
810
|
+ "分析评分要求中: 100%");
|
|
752
|
811
|
}
|
|
753
|
812
|
|
|
754
|
813
|
/**
|
|
755
|
814
|
* 编排章节
|
|
756
|
815
|
* @return
|
|
757
|
816
|
*/
|
|
758
|
|
- public String writeChapters(String prompt, String templatePath) throws IOException {
|
|
|
817
|
+ public String writeChapters(String prompt, String fileName, String agentName) throws IOException {
|
|
759
|
818
|
String chapters2 = generateAnswer(prompt);
|
|
760
|
819
|
|
|
761
|
820
|
//根据技术文档知识库生成二级标题下三级标题
|
|
|
@@ -801,7 +860,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService
|
|
801
|
860
|
"6.n.1 XX\n" +
|
|
802
|
861
|
"6.n 2 XX\n";
|
|
803
|
862
|
String content = generateAnswer(sb);
|
|
804
|
|
- writeTitles(content, templatePath);
|
|
|
863
|
+ writeTitles(content, fileName, agentName);
|
|
805
|
864
|
return content;
|
|
806
|
865
|
}
|
|
807
|
866
|
|
|
|
@@ -812,14 +871,17 @@ public class CmcAgentServiceImpl implements ICmcAgentService
|
|
812
|
871
|
public String generateAnswer(String prompt) throws IOException {
|
|
813
|
872
|
ChatModel chatModel = ChatModel.of(llmServiceUrl)
|
|
814
|
873
|
.model("Qwen")
|
|
|
874
|
+ .timeout(java.time.Duration.ofSeconds(120))
|
|
815
|
875
|
.build();
|
|
816
|
876
|
|
|
817
|
877
|
List<ChatMessage> messages = new ArrayList<>();
|
|
818
|
878
|
messages.add(ChatMessage.ofUser(prompt));
|
|
819
|
879
|
ChatSession chatSession = InMemoryChatSession.builder().messages(messages).build();
|
|
820
|
|
- ChatResponse response = chatModel.prompt(chatSession).call();
|
|
821
|
880
|
|
|
822
|
|
- return response.lastChoice().getMessage().getContent();
|
|
|
881
|
+ Prompt prompt1 = Prompt.of(prompt).attrPut("session", chatSession);
|
|
|
882
|
+ ChatResponse response = chatModel.prompt(prompt1).call();
|
|
|
883
|
+ String content = response.lastChoice().getMessage().getContent();
|
|
|
884
|
+ return content;
|
|
823
|
885
|
}
|
|
824
|
886
|
|
|
825
|
887
|
/**
|
|
|
@@ -893,13 +955,27 @@ public class CmcAgentServiceImpl implements ICmcAgentService
|
|
893
|
955
|
* 写入章节大纲
|
|
894
|
956
|
* @return
|
|
895
|
957
|
*/
|
|
896
|
|
- public void writeTitles(String content, String templatePath) throws IOException {
|
|
|
958
|
+ public void writeTitles(String content, String fileName, String agentName) throws IOException {
|
|
897
|
959
|
List<String> chapters = new ArrayList<>();
|
|
898
|
960
|
String[] contentLines = content.split("\n");
|
|
899
|
961
|
for (String line : contentLines) {
|
|
900
|
962
|
if (line.contains("6."))
|
|
901
|
963
|
chapters.add(line.replace("*", "").replace("#", "").replace("-", ""));
|
|
902
|
964
|
}
|
|
|
965
|
+ String prefixPath = "/upload/agent/" + agentName;
|
|
|
966
|
+ String[] filenameSplit = fileName.split("\\.");
|
|
|
967
|
+ String outputFilename = prefixPath + "/" + fileName
|
|
|
968
|
+ .replace(filenameSplit[filenameSplit.length - 2], filenameSplit[filenameSplit.length - 2] + "_" + agentName);
|
|
|
969
|
+ String templatePath = RuoYiConfig.getProfile() + outputFilename;
|
|
|
970
|
+ Path outputFilePath = Paths.get(templatePath);
|
|
|
971
|
+ Files.deleteIfExists(outputFilePath);
|
|
|
972
|
+ InputStream inputStream = new FileInputStream(RuoYiConfig.getProfile() + "/upload/agent/template/technical.docx");
|
|
|
973
|
+ try (XWPFDocument doc = new XWPFDocument(inputStream)) {
|
|
|
974
|
+ // 保存文档到本地文件系统
|
|
|
975
|
+ try (FileOutputStream out = new FileOutputStream(templatePath)) {
|
|
|
976
|
+ doc.write(out);
|
|
|
977
|
+ }
|
|
|
978
|
+ }
|
|
903
|
979
|
File file = new File(templatePath);
|
|
904
|
980
|
FileInputStream fileInputStream = new FileInputStream(file);
|
|
905
|
981
|
try (XWPFDocument document = new XWPFDocument(fileInputStream)) {
|
|
|
@@ -1010,6 +1086,50 @@ public class CmcAgentServiceImpl implements ICmcAgentService
|
|
1010
|
1086
|
/**
|
|
1011
|
1087
|
* 获取最低级别子标题列表
|
|
1012
|
1088
|
*/
|
|
|
1089
|
+ /**
|
|
|
1090
|
+ * 保存目录到Word文件
|
|
|
1091
|
+ */
|
|
|
1092
|
+ @Override
|
|
|
1093
|
+ public JSONObject writeTitles(JSONObject data) throws IOException {
|
|
|
1094
|
+ JSONObject result = new JSONObject();
|
|
|
1095
|
+ try {
|
|
|
1096
|
+ String filename = data.getString("filename");
|
|
|
1097
|
+ String agentName = data.getString("agentName");
|
|
|
1098
|
+ JSONArray titlesArray = data.getJSONArray("titles");
|
|
|
1099
|
+
|
|
|
1100
|
+ // 构建目录字符串
|
|
|
1101
|
+ StringBuilder titlesContent = new StringBuilder();
|
|
|
1102
|
+ buildTitlesContent(titlesArray, titlesContent);
|
|
|
1103
|
+
|
|
|
1104
|
+ // 调用现有的writeTitles方法写入Word文件
|
|
|
1105
|
+ writeTitles(titlesContent.toString(), filename, agentName);
|
|
|
1106
|
+
|
|
|
1107
|
+ result.put("code", 200);
|
|
|
1108
|
+ result.put("message", "目录保存成功");
|
|
|
1109
|
+ } catch (Exception e) {
|
|
|
1110
|
+ e.printStackTrace();
|
|
|
1111
|
+ result.put("code", 500);
|
|
|
1112
|
+ result.put("message", "目录保存失败: " + e.getMessage());
|
|
|
1113
|
+ }
|
|
|
1114
|
+ return result;
|
|
|
1115
|
+ }
|
|
|
1116
|
+
|
|
|
1117
|
+ /**
|
|
|
1118
|
+ * 构建目录内容字符串
|
|
|
1119
|
+ */
|
|
|
1120
|
+ private void buildTitlesContent(JSONArray titlesArray, StringBuilder content) {
|
|
|
1121
|
+ for (int i = 0; i < titlesArray.size(); i++) {
|
|
|
1122
|
+ JSONObject titleObj = titlesArray.getJSONObject(i);
|
|
|
1123
|
+ String title = titleObj.getString("title");
|
|
|
1124
|
+ content.append(title).append("\n");
|
|
|
1125
|
+
|
|
|
1126
|
+ if (titleObj.containsKey("children")) {
|
|
|
1127
|
+ JSONArray children = titleObj.getJSONArray("children");
|
|
|
1128
|
+ buildTitlesContent(children, content);
|
|
|
1129
|
+ }
|
|
|
1130
|
+ }
|
|
|
1131
|
+ }
|
|
|
1132
|
+
|
|
1013
|
1133
|
public List<String> extractSubTitles(String filename, String question) throws IOException {
|
|
1014
|
1134
|
List<String> subTitles = new ArrayList<>();
|
|
1015
|
1135
|
InputStream fileInputStream = new FileInputStream(filename);
|
|
|
@@ -1152,6 +1272,96 @@ public class CmcAgentServiceImpl implements ICmcAgentService
|
|
1152
|
1272
|
}
|
|
1153
|
1273
|
}
|
|
1154
|
1274
|
|
|
|
1275
|
+ /**
|
|
|
1276
|
+ * 从 DOCX 文档中提取表格内容
|
|
|
1277
|
+ */
|
|
|
1278
|
+ private List<TextSegment> extractTablesFromDocx(XWPFDocument document) {
|
|
|
1279
|
+ List<TextSegment> tableSegments = new ArrayList<>();
|
|
|
1280
|
+ List<XWPFTable> tables = document.getTables();
|
|
|
1281
|
+
|
|
|
1282
|
+ for (int i = 0; i < tables.size(); i++) {
|
|
|
1283
|
+ XWPFTable table = tables.get(i);
|
|
|
1284
|
+ StringBuilder tableContent = new StringBuilder();
|
|
|
1285
|
+
|
|
|
1286
|
+ // 添加表格标识
|
|
|
1287
|
+ tableContent.append("[表格 ").append(i + 1).append("]\n");
|
|
|
1288
|
+
|
|
|
1289
|
+ // 提取表格内容
|
|
|
1290
|
+ for (XWPFTableRow row : table.getRows()) {
|
|
|
1291
|
+ StringBuilder rowContent = new StringBuilder();
|
|
|
1292
|
+ for (XWPFTableCell cell : row.getTableCells()) {
|
|
|
1293
|
+ String cellText = cell.getText().trim();
|
|
|
1294
|
+ if (!cellText.isEmpty()) {
|
|
|
1295
|
+ rowContent.append(cellText).append(" | ");
|
|
|
1296
|
+ }
|
|
|
1297
|
+ }
|
|
|
1298
|
+ if (rowContent.length() > 0) {
|
|
|
1299
|
+ // 移除末尾的分隔符
|
|
|
1300
|
+ if (rowContent.length() >= 3) {
|
|
|
1301
|
+ rowContent.setLength(rowContent.length() - 3);
|
|
|
1302
|
+ }
|
|
|
1303
|
+ tableContent.append(rowContent).append("\n");
|
|
|
1304
|
+ }
|
|
|
1305
|
+ }
|
|
|
1306
|
+
|
|
|
1307
|
+ // 只有当表格内容有实际内容时才添加
|
|
|
1308
|
+ if (tableContent.length() > 10) {
|
|
|
1309
|
+ TextSegment segment = TextSegment.from(tableContent.toString());
|
|
|
1310
|
+ tableSegments.add(segment);
|
|
|
1311
|
+ }
|
|
|
1312
|
+ }
|
|
|
1313
|
+
|
|
|
1314
|
+ return tableSegments;
|
|
|
1315
|
+ }
|
|
|
1316
|
+
|
|
|
1317
|
+ /**
|
|
|
1318
|
+ * 从 DOC 文档中提取表格内容
|
|
|
1319
|
+ */
|
|
|
1320
|
+ private List<TextSegment> extractTablesFromDoc(HWPFDocument document) {
|
|
|
1321
|
+ List<TextSegment> tableSegments = new ArrayList<>();
|
|
|
1322
|
+ Range range = document.getRange();
|
|
|
1323
|
+
|
|
|
1324
|
+ int tableCount = 0;
|
|
|
1325
|
+ StringBuilder tableContent = new StringBuilder();
|
|
|
1326
|
+ boolean inTable = false;
|
|
|
1327
|
+
|
|
|
1328
|
+ for (int i = 0; i < range.numParagraphs(); i++) {
|
|
|
1329
|
+ Paragraph paragraph = range.getParagraph(i);
|
|
|
1330
|
+ String text = paragraph.text().trim();
|
|
|
1331
|
+
|
|
|
1332
|
+ // 检测表格段落
|
|
|
1333
|
+ if (paragraph.isInTable()) {
|
|
|
1334
|
+ if (!inTable) {
|
|
|
1335
|
+ // 开始新表格
|
|
|
1336
|
+ tableCount++;
|
|
|
1337
|
+ tableContent = new StringBuilder();
|
|
|
1338
|
+ tableContent.append("[表格 ").append(tableCount).append("]\n");
|
|
|
1339
|
+ inTable = true;
|
|
|
1340
|
+ }
|
|
|
1341
|
+
|
|
|
1342
|
+ // 添加表格行内容
|
|
|
1343
|
+ if (!text.isEmpty()) {
|
|
|
1344
|
+ tableContent.append(text).append("\n");
|
|
|
1345
|
+ }
|
|
|
1346
|
+ } else if (inTable) {
|
|
|
1347
|
+ // 表格结束
|
|
|
1348
|
+ inTable = false;
|
|
|
1349
|
+ if (tableContent.length() > 10) {
|
|
|
1350
|
+ TextSegment segment = TextSegment.from(tableContent.toString());
|
|
|
1351
|
+ tableSegments.add(segment);
|
|
|
1352
|
+ }
|
|
|
1353
|
+ }
|
|
|
1354
|
+ }
|
|
|
1355
|
+
|
|
|
1356
|
+ // 处理最后一个表格
|
|
|
1357
|
+ if (inTable && tableContent.length() > 10) {
|
|
|
1358
|
+ TextSegment segment = TextSegment.from(tableContent.toString());
|
|
|
1359
|
+ tableSegments.add(segment);
|
|
|
1360
|
+ }
|
|
|
1361
|
+
|
|
|
1362
|
+ return tableSegments;
|
|
|
1363
|
+ }
|
|
|
1364
|
+
|
|
1155
|
1365
|
/**
|
|
1156
|
1366
|
* 按三级标题分割DOC内容
|
|
1157
|
1367
|
*/
|
|
|
@@ -1243,6 +1453,10 @@ public class CmcAgentServiceImpl implements ICmcAgentService
|
|
1243
|
1453
|
splitDocxByLevel3(currentLevel2Content.toString(), xwpfDocument, segments);
|
|
1244
|
1454
|
}
|
|
1245
|
1455
|
}
|
|
|
1456
|
+
|
|
|
1457
|
+ // 提取文档中的所有表格
|
|
|
1458
|
+ List<TextSegment> tableSegments = extractTablesFromDocx(xwpfDocument);
|
|
|
1459
|
+ segments.addAll(tableSegments);
|
|
1246
|
1460
|
}
|
|
1247
|
1461
|
return segments;
|
|
1248
|
1462
|
}
|
|
|
@@ -1289,6 +1503,10 @@ public class CmcAgentServiceImpl implements ICmcAgentService
|
|
1289
|
1503
|
splitDocByLevel3(currentLevel2Content.toString(), hwpfDocument, segments);
|
|
1290
|
1504
|
}
|
|
1291
|
1505
|
}
|
|
|
1506
|
+
|
|
|
1507
|
+ // 提取文档中的所有表格
|
|
|
1508
|
+ List<TextSegment> tableSegments = extractTablesFromDoc(hwpfDocument);
|
|
|
1509
|
+ segments.addAll(tableSegments);
|
|
1292
|
1510
|
}
|
|
1293
|
1511
|
return segments;
|
|
1294
|
1512
|
}
|