Browse Source

获取上传解析进度

lamphua 2 weeks ago
parent
commit
acb273d427

+ 80
- 53
llm-back/ruoyi-agent/src/main/java/com/ruoyi/agent/service/impl/McpServiceImpl.java View File

33
 import org.noear.solon.ai.chat.ChatModel;
33
 import org.noear.solon.ai.chat.ChatModel;
34
 import org.noear.solon.ai.chat.ChatResponse;
34
 import org.noear.solon.ai.chat.ChatResponse;
35
 import org.noear.solon.ai.chat.ChatSession;
35
 import org.noear.solon.ai.chat.ChatSession;
36
-import org.noear.solon.ai.chat.ChatSessionDefault;
37
 import org.noear.solon.ai.chat.message.AssistantMessage;
36
 import org.noear.solon.ai.chat.message.AssistantMessage;
38
 import org.noear.solon.ai.chat.message.ChatMessage;
37
 import org.noear.solon.ai.chat.message.ChatMessage;
39
 import org.noear.solon.ai.chat.session.InMemoryChatSession;
38
 import org.noear.solon.ai.chat.session.InMemoryChatSession;
70
     {
69
     {
71
             try {
70
             try {
72
                 templatePath = templatePath.replace("/dev-api/profile", Solon.cfg().getProperty("cmc.profile"));
71
                 templatePath = templatePath.replace("/dev-api/profile", Solon.cfg().getProperty("cmc.profile"));
73
-                title = String.join(",", extractSubTitles(templatePath, title));
72
+                List<String> subTitles = extractSubTitles(templatePath, title);
74
 //                List<JSONObject> contexts = retrieveFromMilvus(milvusClient, embeddingModel, collectionName, title, 10);
73
 //                List<JSONObject> contexts = retrieveFromMilvus(milvusClient, embeddingModel, collectionName, title, 10);
75
 //                return generateAnswerWithDocumentAndCollection(embeddingModel, agentName, templatePath, title, contexts, llmServiceUrl);
74
 //                return generateAnswerWithDocumentAndCollection(embeddingModel, agentName, templatePath, title, contexts, llmServiceUrl);
76
-                return generateAnswerWithDocumentAndCollection(embeddingModel, agentName, templatePath, title, new ArrayList<>(), llmServiceUrl);
75
+                return generateAnswerWithDocumentAndCollection(embeddingModel, agentName, templatePath, subTitles, new ArrayList<>());
77
             } catch (IOException e) {
76
             } catch (IOException e) {
78
                 throw new RuntimeException(e);
77
                 throw new RuntimeException(e);
79
             }
78
             }
98
     /**
97
     /**
99
      * 调用LLM生成回答
98
      * 调用LLM生成回答
100
      */
99
      */
101
-    public AssistantMessage generateAnswerWithDocumentAndCollection(EmbeddingModel embeddingModel, String agentName, String templatePath, String question, List<JSONObject> contexts, String llmServiceUrl) throws IOException {
100
+    public AssistantMessage generateAnswerWithDocumentAndCollection(EmbeddingModel embeddingModel, String agentName, String templatePath, List<String> titles, List<JSONObject> contexts) throws IOException {
102
         StringBuilder sb = new StringBuilder("招标文件内容:\n\n");
101
         StringBuilder sb = new StringBuilder("招标文件内容:\n\n");
103
         String filename = templatePath.replace("_" + agentName, "");
102
         String filename = templatePath.replace("_" + agentName, "");
104
         File profilePath = new File(filename);
103
         File profilePath = new File(filename);
110
         List<TextSegment> segments = splitDocument(profilePath);
109
         List<TextSegment> segments = splitDocument(profilePath);
111
         List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
110
         List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
112
         embeddingStore.addAll(embeddings, segments);
111
         embeddingStore.addAll(embeddings, segments);
113
-        Embedding queryEmbedding = embeddingModel.embed(question).content();
114
-        EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
115
-                .queryEmbedding(queryEmbedding)
116
-                .minScore(0.7)
117
-                .build();
118
-        List<EmbeddingMatch<TextSegment>> results = embeddingStore.search(embeddingSearchRequest).matches();
119
-        results.sort(Comparator.comparingDouble(EmbeddingMatch<TextSegment>::score).reversed());
120
-        for (EmbeddingMatch<TextSegment> embeddingMatch : results) {
121
-            String requests = embeddingMatch.embedded().toString();
122
-            sb.append(requests).append("\n\n");
112
+        StringBuilder content = new StringBuilder();
113
+        for (String title : titles) {
114
+            Embedding queryEmbedding = embeddingModel.embed(title).content();
115
+            EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
116
+                    .queryEmbedding(queryEmbedding)
117
+                    .minScore(0.7)
118
+                    .build();
119
+            List<EmbeddingMatch<TextSegment>> results = embeddingStore.search(embeddingSearchRequest).matches();
120
+            results.sort(Comparator.comparingDouble(EmbeddingMatch<TextSegment>::score).reversed());
121
+            for (EmbeddingMatch<TextSegment> embeddingMatch : results) {
122
+                String requests = embeddingMatch.embedded().toString();
123
+                sb.append(requests).append("\n\n");
123
 
124
 
125
+            }
126
+            sb.append("针对本项目招标文件内容,补全以下章节部分:\n\n").append(title);
127
+            content.append(generateAnswer(sb.toString()));
124
         }
128
         }
125
-        sb.append("针对本项目招标文件内容,补全以下章节部分:\n\n").append(question);
129
+        String absolutePath = templatePath.replace("/dev-api/profile", Solon.cfg().getProperty("cmc.profile"));
130
+        writeContent(content.toString(), titles, absolutePath);
126
 //        for (JSONObject context : contexts) {
131
 //        for (JSONObject context : contexts) {
127
 //            sb.append("文件").append(": ")
132
 //            sb.append("文件").append(": ")
128
 //                    .append(context.getString("file_name")).append("\n\n")
133
 //                    .append(context.getString("file_name")).append("\n\n")
129
 //                    .append("段落格式").append(": ")
134
 //                    .append("段落格式").append(": ")
130
 //                    .append(context.getString("content")).append("\n\n");
135
 //                    .append(context.getString("content")).append("\n\n");
131
 //        }
136
 //        }
132
-        return generateAnswer(sb.toString(), question, templatePath, llmServiceUrl);
137
+        content.append( "招标文件分析完成,章节内容已写入【<a href='")
138
+                .append(templatePath.replace(Solon.cfg().getProperty("cmc.profile"), "/dev-api/profile"))
139
+                .append("'> 技术文件" + "</a>】,请查阅\n\n")
140
+                .append("如需修改,请输入技术文件已有内容的章节标题\n\n")
141
+                .append(extractTitles(templatePath));
142
+        return ChatMessage.ofAssistant(content.toString());
133
     }
143
     }
134
 
144
 
135
     /**
145
     /**
136
      * 调用LLM生成回答
146
      * 调用LLM生成回答
137
      * @return
147
      * @return
138
      */
148
      */
139
-    public AssistantMessage generateAnswer(String prompt, String question, String templatePath, String llmServiceUrl) throws IOException {
149
+    public String generateAnswer(String prompt) throws IOException {
140
         ChatModel chatModel = ChatModel.of(llmServiceUrl)
150
         ChatModel chatModel = ChatModel.of(llmServiceUrl)
141
-                
142
                 .model("Qwen2.5-1.5B-Instruct")
151
                 .model("Qwen2.5-1.5B-Instruct")
143
                 .build();
152
                 .build();
144
 
153
 
154
+
145
         List<ChatMessage> messages = new ArrayList<>();
155
         List<ChatMessage> messages = new ArrayList<>();
146
         messages.add(ChatMessage.ofUser(prompt));
156
         messages.add(ChatMessage.ofUser(prompt));
147
         ChatSession chatSession =  InMemoryChatSession.builder().messages(messages).build();
157
         ChatSession chatSession =  InMemoryChatSession.builder().messages(messages).build();
158
+        chatSession.addMessage(messages);
148
         ChatResponse response = chatModel.prompt(chatSession).call();
159
         ChatResponse response = chatModel.prompt(chatSession).call();
149
-        String content = response.lastChoice().getMessage().getContent() + "\n\n" +
150
-                "招标文件分析完成,章节内容已写入【<a href='" + templatePath.replace(Solon.cfg().getProperty("cmc.profile"), "/dev-api/profile") + "'> 技术文件" + "</a>】,请查阅\n\n" +
151
-                "如需修改,请输入技术文件已有内容的章节标题\n\n" +
152
-                extractTitles(templatePath);
153
-        String absolutePath = templatePath.replace("/dev-api/profile", Solon.cfg().getProperty("cmc.profile"));
154
-        writeContent(response.lastChoice().getMessage().getContent(), question, absolutePath);
155
-        return ChatMessage.ofAssistant(content);
160
+
161
+        return response.lastChoice().getMessage().getContent();
156
     }
162
     }
157
 
163
 
158
     /**
164
     /**
159
      * 写入章节内容
165
      * 写入章节内容
160
      * @return
166
      * @return
161
      */
167
      */
162
-    public void writeContent(String content, String question, String absolutePath) throws IOException {
168
+    public void writeContent(String content, List<String> titles, String absolutePath) throws IOException {
163
         String[] contentLines = content.split("\n");
169
         String[] contentLines = content.split("\n");
164
         Map<String, String> map = new HashMap<>();
170
         Map<String, String> map = new HashMap<>();
165
-        String[] titles = question.split(",");
166
         File file = new File(absolutePath);
171
         File file = new File(absolutePath);
167
         FileInputStream fileInputStream = new FileInputStream(file);
172
         FileInputStream fileInputStream = new FileInputStream(file);
168
         try (XWPFDocument document = new XWPFDocument(fileInputStream)) {
173
         try (XWPFDocument document = new XWPFDocument(fileInputStream)) {
169
-            for (int i = 0; i < titles.length; i++) {
170
-                int startIndex = Arrays.asList(contentLines).indexOf(titles[i]);
174
+            for (int i = 0; i < titles.size(); i++) {
175
+                int startIndex = Arrays.asList(contentLines).indexOf(titles.get(i));
171
                 StringBuilder text = new StringBuilder();
176
                 StringBuilder text = new StringBuilder();
172
                 if (startIndex >= 0) {
177
                 if (startIndex >= 0) {
173
-                    if (i < titles.length - 1) {
174
-                        int endIndex = Arrays.asList(contentLines).indexOf(titles[i + 1]);
178
+                    if (i < titles.size() - 1) {
179
+                        int endIndex = Arrays.asList(contentLines).indexOf(titles.get(i + 1));
175
                         for (int c = startIndex + 1; c < endIndex; c++) {
180
                         for (int c = startIndex + 1; c < endIndex; c++) {
176
                             text.append(contentLines[c]).append("\n\n");
181
                             text.append(contentLines[c]).append("\n\n");
177
                         }
182
                         }
185
                 }
190
                 }
186
                 else
191
                 else
187
                     text.append(content);
192
                     text.append(content);
188
-                map.put(titles[i], text.toString());
193
+                map.put(titles.get(i), text.toString());
189
             }
194
             }
190
 
195
 
191
             List<Integer> positions = new ArrayList<>();
196
             List<Integer> positions = new ArrayList<>();
218
     }
223
     }
219
 
224
 
220
     /**
225
     /**
221
-     * 获取二级标题下三级标题列表
226
+     * 获取最低级别子标题列表
222
      */
227
      */
223
     public List<String> extractSubTitles(String filename, String question) throws IOException {
228
     public List<String> extractSubTitles(String filename, String question) throws IOException {
224
         List<String> subTitles = new ArrayList<>();
229
         List<String> subTitles = new ArrayList<>();
225
-        boolean inTargetSection = false;
226
         InputStream fileInputStream = new FileInputStream(filename);
230
         InputStream fileInputStream = new FileInputStream(filename);
231
+
232
+        boolean foundParent = false;
233
+        int parentLevel = -1;
234
+        // 用于跟踪当前路径
235
+        List<XWPFParagraph> currentPath = new ArrayList<>();
236
+
227
         try (XWPFDocument document = new XWPFDocument(fileInputStream)) {
237
         try (XWPFDocument document = new XWPFDocument(fileInputStream)) {
228
-            for (XWPFParagraph paragraph : document.getParagraphs()) {
229
-                String text = paragraph.getText().trim();
230
-                if (paragraph.getStyle() != null) {
231
-                    // 判断主标题
232
-                    if (paragraph.getStyle().equals("3") &&
233
-                            text.contains(question)) {
234
-                        inTargetSection = true;
235
-                        continue;
236
-                    }
237
 
238
 
238
-                    // 如果已经在目标节中,收集标题3级别的子标题
239
-                    if (inTargetSection) {
240
-                        if (paragraph.getStyle().equals("4")) {
239
+            for (XWPFParagraph  paragraph : document.getParagraphs()) {
240
+                String text = paragraph.getText();
241
+                int level = Integer.parseInt(paragraph.getStyle());
242
+
243
+                // 维护当前路径
244
+                while (!currentPath.isEmpty() && Integer.parseInt(currentPath.get(currentPath.size() - 1).getStyle()) >= level) {
245
+                    currentPath.remove(currentPath.size() - 1);
246
+                }
247
+                currentPath.add(paragraph);
248
+                if (foundParent) {
249
+                    if (level > parentLevel) { // 是子标题
250
+                        if (isLeafNode(document, paragraph, level)) {
241
                             subTitles.add(text);
251
                             subTitles.add(text);
242
                         }
252
                         }
243
-                        // 遇到下一个Heading1则退出
244
-                        else if (paragraph.getStyle().equals("3")) {
245
-                            break;
246
-                        }
253
+                    } else {
254
+                        // 遇到同级或更高级别的标题,停止搜索
255
+                        break;
247
                     }
256
                     }
257
+                } else if (text.equals(question)) {
258
+                    foundParent = true;
259
+                    parentLevel = level;
248
                 }
260
                 }
249
             }
261
             }
262
+
263
+
264
+            return subTitles;
265
+
250
         }
266
         }
251
-        if (subTitles.size() == 0)
252
-            subTitles.add(question);
253
-        return subTitles;
254
     }
267
     }
255
 
268
 
269
+    // 检查一个标题是否是叶子节点(没有更低级别的子标题)
270
+    private boolean isLeafNode(XWPFDocument doc, XWPFParagraph p, int level) {
271
+        int index = doc.getPosOfParagraph(p);
272
+        if (index == -1 || index >= doc.getParagraphs().size()-1) {
273
+            return true;
274
+        }
275
+
276
+        // 检查后续段落
277
+        XWPFParagraph nextP = doc.getParagraphs().get(index + 1);
278
+        int nextLevel = Integer.parseInt(nextP.getStyle());
279
+        // 遇到同级或更高级别标题,是叶子节点
280
+        return nextLevel <= level; // 存在更低级别的标题,不是叶子节点
281
+
282
+    }
256
     /**
283
     /**
257
      * 获取二、三级标题列表
284
      * 获取二、三级标题列表
258
      */
285
      */

+ 8
- 0
llm-back/ruoyi-llm/src/main/java/com/ruoyi/web/llm/controller/CmcAgentController.java View File

99
         return success(cmcAgentService.uploadDocumentList(fileList, agentName));
99
         return success(cmcAgentService.uploadDocumentList(fileList, agentName));
100
     }
100
     }
101
 
101
 
102
+    /**
103
+     * 获取上传进度
104
+     */
105
+    @GetMapping(value = "/getProcess")
106
+    public AjaxResult getProcess() {
107
+        return success(cmcAgentService.getProcess());
108
+    }
109
+
102
     /**
110
     /**
103
      * 新增智能体
111
      * 新增智能体
104
      */
112
      */

+ 7
- 64
llm-back/ruoyi-llm/src/main/java/com/ruoyi/web/llm/controller/McpController.java View File

61
                     .withHost("192.168.28.188")
61
                     .withHost("192.168.28.188")
62
                     .withPort(19530)
62
                     .withPort(19530)
63
                     .build());
63
                     .build());
64
-//
65
-//    /**
66
-//     * 同步问答
67
-//     * @return
68
-//     */
69
-//    @GetMapping("/answer")
70
-//    public AssistantMessage answer(String question) throws IOException {
71
-//        McpClientProvider clientProvider = McpClientProvider.builder()
72
-//                .apiUrl("http://localhost:8080/llm/mcp/sse")
73
-//                .build();
74
-//        ChatModel chatModel = ChatModel.of(llmServiceUrl)
75
-//                .model("Qwen2.5-1.5B-Instruct")
76
-//                .defaultToolsAdd(clientProvider)
77
-//                .build();
78
-//        ChatResponse response = chatModel.prompt(question).call();
79
-//        String resultContent = response.lastChoice().getMessage().getResultContent();
80
-//        AssistantMessage assistantMessage = new AssistantMessage(resultContent);
81
-//        if (resultContent.startsWith("<tool_call>")) {
82
-//            String content = resultContent.replace("<tool_call>\n", "").replace("\n</tool_call>", "");
83
-//            JSONObject jsonObject = JSONObject.parseObject(content);
84
-//            String name = jsonObject.getString("name");
85
-//            JSONObject arguments = jsonObject.getJSONObject("arguments");
86
-//            resultContent = clientProvider.callToolAsText(name, arguments).getContent();
87
-//            assistantMessage = new AssistantMessage(resultContent);
88
-//        }
89
-//        return assistantMessage;
90
-//    }
91
 
64
 
92
     /**
65
     /**
93
      * 自动调用mcp工具问答
66
      * 自动调用mcp工具问答
125
                 arguments.put("collectionName", "technical");
98
                 arguments.put("collectionName", "technical");
126
                 String agentName = cmcAgentService.selectCmcAgentByAgentId(cmcTopicService.selectCmcTopicByTopicId(topicId).getAgentId()).getAgentName();
99
                 String agentName = cmcAgentService.selectCmcAgentByAgentId(cmcTopicService.selectCmcTopicByTopicId(topicId).getAgentId()).getAgentName();
127
                 arguments.put("agentName", agentName);
100
                 arguments.put("agentName", agentName);
101
+                arguments.put("title", question);
102
+            }
103
+            if (arguments.getString("templatePath").contains("招标") || arguments.getString("templatePath").contains("询价")) {
104
+                arguments.put("collectionName", "technical");
105
+                String agentName = cmcAgentService.selectCmcAgentByAgentId(cmcTopicService.selectCmcTopicByTopicId(topicId).getAgentId()).getAgentName();
106
+                arguments.put("agentName", agentName);
107
+                arguments.put("title", question);
128
             }
108
             }
129
             resultContent = clientProvider.callToolAsText(name, arguments).getContent();
109
             resultContent = clientProvider.callToolAsText(name, arguments).getContent();
130
             assistantMessage = new AssistantMessage(resultContent);
110
             assistantMessage = new AssistantMessage(resultContent);
134
         return assistantMessage;
114
         return assistantMessage;
135
     }
115
     }
136
 
116
 
137
-    /**
138
-     * 调用LLM+RAG(外部文件+知识库)生成回答
139
-     */
140
-    @GetMapping("/answerWithDocument")
141
-    public Flux<AssistantMessage> answerWithDocumentAndCollection(String collectionName, String topicId, String question) throws IOException
142
-    {
143
-        question = String.join(",", langChainMilvusService.extractSubTitles(RuoYiConfig.getProfile() + "/upload/agent/template/technical.docx", question));
144
-        List<JSONObject> requests = langChainMilvusService.retrieveFromMilvus(milvusClient, embeddingModel, collectionName, question, 10);
145
-        return langChainMilvusService.generateAnswerWithDocumentAndCollection(embeddingModel, topicId, question, requests, llmServiceUrl);
146
-    }
147
-
148
-    /**
149
-     * 根据招标文件要求编写投标文件技术方案
150
-     * @return
151
-     */
152
-    @GetMapping("/tech")
153
-    public AssistantMessage tech(MultipartFile file) throws IOException {
154
-        McpClientProvider clientProvider = McpClientProvider.builder()
155
-                    .apiUrl("http://localhost:8080/llm/mcp/sse")
156
-                    .build();
157
-        ChatModel chatModel = ChatModel.of(llmServiceUrl)
158
-                
159
-                .model("DeepSeek-R1-Distill-Qwen-1.5B")
160
-                .defaultToolsAdd(clientProvider)
161
-                .build();
162
-        Map<String,Object> path = new HashMap<>();
163
-        String content = clientProvider.callToolAsText("openDocument", path).getContent();
164
-        Map<String,Object> request = new HashMap<>();
165
-        request.put("request", content);
166
-        List<ChatMessage> messages = clientProvider.getPromptAsMessages("askQuestion", request);
167
-        ChatResponse response = chatModel.prompt(messages).call();
168
-        System.out.println(response.getChoices().get(0).getMessage());
169
-        Map<String,Object> write = new HashMap<>();
170
-        write.put("resultContent", response.getChoices().get(0).getMessage().getResultContent());
171
-        String res = clientProvider.callToolAsText("writeTechnicalPlan", write).getContent();
172
-        return response.getChoices().get(0).getMessage();
173
-    }
174
 
117
 
175
 }
118
 }

+ 7
- 0
llm-back/ruoyi-system/src/main/java/com/ruoyi/llm/service/ICmcAgentService.java View File

55
      */
55
      */
56
     public String uploadDocumentList(MultipartFile[] fileList, String agentName) throws IOException;
56
     public String uploadDocumentList(MultipartFile[] fileList, String agentName) throws IOException;
57
 
57
 
58
+    /**
59
+     * 获取进度
60
+     *
61
+     * @return 结果
62
+     */
63
+    public String getProcess();
64
+
58
     /**
65
     /**
59
      * 新增智能体
66
      * 新增智能体
60
      * 
67
      * 

+ 67
- 30
llm-back/ruoyi-system/src/main/java/com/ruoyi/llm/service/impl/CmcAgentServiceImpl.java View File

1
 package com.ruoyi.llm.service.impl;
1
 package com.ruoyi.llm.service.impl;
2
 
2
 
3
 import java.io.*;
3
 import java.io.*;
4
+import java.nio.file.Files;
5
+import java.nio.file.Path;
6
+import java.nio.file.Paths;
4
 import java.util.*;
7
 import java.util.*;
5
 import java.util.stream.Collectors;
8
 import java.util.stream.Collectors;
6
 
9
 
70
     @Autowired
73
     @Autowired
71
     private CmcChatMapper cmcChatMapper;
74
     private CmcChatMapper cmcChatMapper;
72
 
75
 
76
+    private String processValue = "";
77
+
73
     private static final EmbeddingModel embeddingModel = new BgeSmallZhV15EmbeddingModel();
78
     private static final EmbeddingModel embeddingModel = new BgeSmallZhV15EmbeddingModel();
74
 
79
 
75
     private static final String llmServiceUrl = "http://192.168.28.188:8000/v1/chat/completions";
80
     private static final String llmServiceUrl = "http://192.168.28.188:8000/v1/chat/completions";
136
         File transferFile = new File(profilePath + "/" + file.getOriginalFilename());
141
         File transferFile = new File(profilePath + "/" + file.getOriginalFilename());
137
         if (!transferFile.exists())
142
         if (!transferFile.exists())
138
             file.transferTo(transferFile);
143
             file.transferTo(transferFile);
144
+        processValue = "上传完成:100%";
139
         CmcDocument cmcDocument = new CmcDocument();
145
         CmcDocument cmcDocument = new CmcDocument();
140
         cmcDocument.setDocumentId(new SnowFlake().generateId());
146
         cmcDocument.setDocumentId(new SnowFlake().generateId());
141
         cmcDocument.setChatId(chatId);
147
         cmcDocument.setChatId(chatId);
154
                     .replace(filenameSplit[filenameSplit.length - 2], filenameSplit[filenameSplit.length - 2] + "_" + agentName);
160
                     .replace(filenameSplit[filenameSplit.length - 2], filenameSplit[filenameSplit.length - 2] + "_" + agentName);
155
             if (file.getOriginalFilename().endsWith(".doc"))
161
             if (file.getOriginalFilename().endsWith(".doc"))
156
                 outputFilename = outputFilename.replace(".doc", ".docx");
162
                 outputFilename = outputFilename.replace(".doc", ".docx");
163
+            Path outputFilePath = Paths.get(RuoYiConfig.getProfile() + outputFilename);
164
+            Files.deleteIfExists(outputFilePath);
157
             InputStream fileInputStream = new FileInputStream(RuoYiConfig.getProfile() + "/upload/agent/template/technical.docx");
165
             InputStream fileInputStream = new FileInputStream(RuoYiConfig.getProfile() + "/upload/agent/template/technical.docx");
158
             try (XWPFDocument doc = new XWPFDocument(fileInputStream)) {
166
             try (XWPFDocument doc = new XWPFDocument(fileInputStream)) {
159
                 // 保存文档到本地文件系统
167
                 // 保存文档到本地文件系统
163
             }
171
             }
164
             String chapters = generateAnswerWithDocument(profilePath + "/" + file.getOriginalFilename(),
172
             String chapters = generateAnswerWithDocument(profilePath + "/" + file.getOriginalFilename(),
165
                     RuoYiConfig.getProfile() + outputFilename, "工作大纲");
173
                     RuoYiConfig.getProfile() + outputFilename, "工作大纲");
166
-            message = "好的,我已经收到您上传的招标文件,我将给您提供技术文件模板,您可点击进行预览:" +
167
-                    "【<a href='/profile" + outputFilename + "'> 模版 " + "</a>】\n\n" +
168
-                    chapters + "\n\n" +
169
-                    "请问您需要哪个章节撰写的帮助?";
174
+            message = "好的,我已经收到您上传的招标文件。\n\n"+ chapters + "\n\n" +
175
+                    "若您对章节标题有异议,请打开" + "【<a href='/profile" + outputFilename + "'> 技术文件 " + "</a>】" + "进行修改,后续将根据修改后的章节标题,帮您生成对应章节内容。\n\n" +
176
+                    "请问您需要哪个章节撰写的帮助?\n" +
177
+                    "如需单个章节,您可以复制章节标题名称至对话输入框,发送给我;如需全部章节,请输入 技术文件 。\n\n" +
178
+                    "思考时间可能较长,请耐心等待,若提示未检测工具,麻烦重新发送,谢谢!\n";
170
         }
179
         }
171
         jsonObject.put("assistantMessage", message);
180
         jsonObject.put("assistantMessage", message);
172
         return jsonObject;
181
         return jsonObject;
191
         return "上传成功";
200
         return "上传成功";
192
     }
201
     }
193
 
202
 
203
+    /**
204
+     * 上传多文件
205
+     *
206
+     * @return 结果
207
+     */
208
+    public String getProcess() {
209
+        return processValue;
210
+    }
211
+
194
     /**
212
     /**
195
      * 新增智能体
213
      * 新增智能体
196
      * 
214
      * 
262
             String requests = embeddingMatch.embedded().toString();
280
             String requests = embeddingMatch.embedded().toString();
263
             sb.append(requests).append("\n\n");
281
             sb.append(requests).append("\n\n");
264
         }
282
         }
283
+        //根据招标文件工作大纲要求生成二级标题
265
         sb.append("请根据上述招标文件内容,严格按以下格式列出").append(question).append(":\n")
284
         sb.append("请根据上述招标文件内容,严格按以下格式列出").append(question).append(":\n")
266
                 .append("6.1 XX\n" +
285
                 .append("6.1 XX\n" +
267
                 "6.2 XX\n" +
286
                 "6.2 XX\n" +
274
      * @return
293
      * @return
275
      */
294
      */
276
     public String writeChapters(String prompt, String templatePath) throws IOException {
295
     public String writeChapters(String prompt, String templatePath) throws IOException {
277
-        String chapter2 = generateAnswer(prompt);
278
-
279
-        StringBuilder sb = new StringBuilder("二级标题如下:\n" + chapter2 + "\n 请根据参考内容编排三级标题,严格按以下格式列出")
280
-                .append(":\n").append("6.1.1 XX\n" +
281
-                        "6.1.2 XX\n" +
282
-                        "6.1.3 XX");
283
-        List<JSONObject> contexts = retrieveFromMilvus("technical", chapter2, 10);
284
-        for (JSONObject context : contexts) {
285
-            sb.append("参考内容").append(": ")
286
-                    .append(context.getString("content")).append("\n\n");
296
+        String chapters2 = generateAnswer(prompt);
297
+
298
+        //根据技术文档知识库生成二级标题下三级标题
299
+        StringBuilder chapter3 = new StringBuilder();
300
+        List<String> chapter2List = new ArrayList<>();
301
+        String[] contentLines = chapters2.split("\n");
302
+        for (String line : contentLines) {
303
+            if (line.contains("6."))
304
+                chapter2List.add(line.replace("*", "").replace("#", "").replace("-", ""));
305
+        }
306
+        for (String chapter2 : chapter2List) {
307
+            String orderNum = chapter2.split(" ")[0];
308
+            StringBuilder sb = new StringBuilder("二级标题如下:\n").append(chapter2)
309
+                    .append("\n 请根据参考内容编排三级标题,严格按以下格式列出:\n")
310
+                    .append(orderNum).append(".1 XX\n")
311
+                    .append(orderNum).append(".2 XX\n")
312
+                    .append(orderNum).append(".3 XX\n");
313
+
314
+            List<JSONObject> contexts = retrieveFromMilvus("technical", chapter2, 10);
315
+            for (JSONObject context : contexts) {
316
+                sb.append("参考内容").append(": ")
317
+                        .append(context.getString("content")).append("\n\n");
318
+            }
319
+            chapter3.append(generateAnswer(sb.toString()));
320
+            processValue = "分析中:" + Double.parseDouble(String.format("%.2f%n", (double) (chapter2List.indexOf(chapter2) + 1)  / chapter2List.size() * 100)) + "%";
287
         }
321
         }
288
-        String chapter3 = generateAnswer(sb.toString());
289
-        sb = new StringBuilder("二级标题如下:\n" + chapter2 + "\n 三级标题如下:" + chapter3 + "帮我合并二三级标题,严格按以下格式列出")
290
-                .append(":\n").append("6.1 XX\n" +
291
-                        "6.1.1 XX\n" +
292
-                        "6.1.2 XX\n"+
293
-                        "6.2 XX\n" +
294
-                        "6.3 XX");
295
-        String content = generateAnswer(sb.toString());
322
+        String sb = "二级标题如下:\n" + chapters2 +
323
+                "\n 三级标题如下:" + chapter3 +
324
+                "帮我合并三级标题,严格按以下格式列出:\n" +
325
+                "6 技术文件\n" +
326
+                "6.1 XX\n" +
327
+                "6.1.1 XX\n" +
328
+                "6.1.2 XX\n" +
329
+                "6.2 XX\n" +
330
+                "6.2.1 XX";
331
+        String content = generateAnswer(sb);
296
         writeContent(content, templatePath);
332
         writeContent(content, templatePath);
297
         return content;
333
         return content;
298
     }
334
     }
302
      * @return
338
      * @return
303
      */
339
      */
304
     public String generateAnswer(String prompt) throws IOException {
340
     public String generateAnswer(String prompt) throws IOException {
305
-        ChatModel chatModel = ChatModel.of(llmServiceUrl).model("Qwen2.5-1.5B-Instruct").build();
341
+        ChatModel chatModel = ChatModel.of(llmServiceUrl)
342
+                .model("Qwen2.5-1.5B-Instruct")
343
+                .build();
306
 
344
 
307
         List<ChatMessage> messages = new ArrayList<>();
345
         List<ChatMessage> messages = new ArrayList<>();
308
         messages.add(ChatMessage.ofUser(prompt));
346
         messages.add(ChatMessage.ofUser(prompt));
383
         try (XWPFDocument document = new XWPFDocument(fileInputStream)) {
421
         try (XWPFDocument document = new XWPFDocument(fileInputStream)) {
384
            for (int i = 0; i < chapters.size(); i++) {
422
            for (int i = 0; i < chapters.size(); i++) {
385
                XWPFParagraph contentParagraph = document.createParagraph();
423
                XWPFParagraph contentParagraph = document.createParagraph();
386
-               contentParagraph.setStyle("3");
387
-               boolean title2 = chapters.get(i).indexOf(".") == chapters.get(i).lastIndexOf(".");
388
-               if (!title2)
389
-                   contentParagraph.setStyle("4");
424
+               int dotNum = chapters.get(i).split("\\.").length - 1;
425
+               contentParagraph.setStyle(String.valueOf(dotNum + 2));
390
 
426
 
391
                XWPFRun run = contentParagraph.createRun();
427
                XWPFRun run = contentParagraph.createRun();
392
-               if (chapters.get(i).contains(" "))
428
+               if (chapters.get(i).split(" ").length > 1)
393
                    run.setText(chapters.get(i).split(" ")[1]);
429
                    run.setText(chapters.get(i).split(" ")[1]);
394
-               run.addBreak();
430
+               if (dotNum > 1)
431
+                   run.addBreak();
395
                if (i < chapters.size() - 1 && chapters.get(i + 1).indexOf(".") == chapters.get(i + 1).lastIndexOf("."))
432
                if (i < chapters.size() - 1 && chapters.get(i + 1).indexOf(".") == chapters.get(i + 1).lastIndexOf("."))
396
                    run.addBreak(BreakType.PAGE);
433
                    run.addBreak(BreakType.PAGE);
397
            }
434
            }
403
     }
440
     }
404
 
441
 
405
     /**
442
     /**
406
-     * 检索知识库
443
+     * 分割文档
407
      */
444
      */
408
     private List<TextSegment> splitDocument(File transferFile) throws IOException {
445
     private List<TextSegment> splitDocument(File transferFile) throws IOException {
409
         // 加载文档
446
         // 加载文档

+ 8
- 0
llm-ui/src/api/llm/agent.js View File

81
   })
81
   })
82
 }
82
 }
83
 
83
 
84
+// 获取上传进度
85
+export function getProcessValue() {
86
+  return request({
87
+    url: '/llm/agent/getProcess',
88
+    method: 'get'
89
+  })
90
+}
91
+
84
 //获取开场白, agentName:智能体名称
92
 //获取开场白, agentName:智能体名称
85
 export function opening(agentName) {
93
 export function opening(agentName) {
86
   return request({
94
   return request({

+ 94
- 63
llm-ui/src/views/llm/agent/AgentDetail.vue View File

1
 <!--
1
 <!--
2
  * @Author: wrh
2
  * @Author: wrh
3
  * @Date: 2025-01-01 00:00:00
3
  * @Date: 2025-01-01 00:00:00
4
- * @LastEditors: Please set LastEditors
5
- * @LastEditTime: 2025-08-01 14:33:27
4
+ * @LastEditors: wrh
5
+ * @LastEditTime: 2025-08-15 17:13:15
6
 -->
6
 -->
7
 <template>
7
 <template>
8
   <div class="agent-detail-container" v-loading="loading">
8
   <div class="agent-detail-container" v-loading="loading">
100
                       确认上传
100
                       确认上传
101
                     </el-button>
101
                     </el-button>
102
                   </div>
102
                   </div>
103
+                  <div v-if="percentageDisplay">
104
+                    <el-progress :percentage="percentage" :stroke-width="25" :text-inside="true">
105
+                      <el-button text>{{ progress }}</el-button>
106
+                    </el-progress>
107
+                  </div>
103
                 </div>
108
                 </div>
104
               </div>
109
               </div>
105
             </div>
110
             </div>
122
                 <div v-else class="message-text">{{ message.content }}</div>
127
                 <div v-else class="message-text">{{ message.content }}</div>
123
                 <div class="message-actions">
128
                 <div class="message-actions">
124
                   <span class="message-time">{{ message.timestamp }}</span>
129
                   <span class="message-time">{{ message.timestamp }}</span>
125
-                  <el-button v-if="message.canRetry" type="text" size="small" @click="retryMessage(message)" class="retry-btn">
126
-                    <el-icon><Refresh /></el-icon>
130
+                  <el-button v-if="message.canRetry" type="text" size="small" @click="retryMessage(message)"
131
+                    class="retry-btn">
132
+                    <el-icon>
133
+                      <Refresh />
134
+                    </el-icon>
127
                     重新发送
135
                     重新发送
128
                   </el-button>
136
                   </el-button>
129
                 </div>
137
                 </div>
172
 import { ref, reactive, watch, nextTick, computed, getCurrentInstance } from 'vue';
180
 import { ref, reactive, watch, nextTick, computed, getCurrentInstance } from 'vue';
173
 import { ElMessage, ElMessageBox } from 'element-plus';
181
 import { ElMessage, ElMessageBox } from 'element-plus';
174
 import { Delete, Refresh } from '@element-plus/icons-vue';
182
 import { Delete, Refresh } from '@element-plus/icons-vue';
175
-import { getAgent, opening, uploadFile } from '@/api/llm/agent';
183
+import { getAgent, opening, uploadFile, getProcessValue } from '@/api/llm/agent';
176
 import { answer } from '@/api/llm/mcp';
184
 import { answer } from '@/api/llm/mcp';
177
 import { listTopic, getTopic, delTopic, addTopic, updateTopic } from "@/api/llm/topic";
185
 import { listTopic, getTopic, delTopic, addTopic, updateTopic } from "@/api/llm/topic";
178
 import { listChat, addChat, updateChat } from "@/api/llm/chat";
186
 import { listChat, addChat, updateChat } from "@/api/llm/chat";
202
 const topicList = ref([])
210
 const topicList = ref([])
203
 const currentTopicId = ref(null)
211
 const currentTopicId = ref(null)
204
 const chatTitle = ref('智能体新对话')
212
 const chatTitle = ref('智能体新对话')
213
+const percentage = ref(0);
214
+const progress = ref("");
215
+const percentageDisplay = ref(false);
205
 
216
 
206
 // 聊天内文件上传相关
217
 // 聊天内文件上传相关
207
 const chatFileList = ref([]) // 聊天内的文件列表
218
 const chatFileList = ref([]) // 聊天内的文件列表
409
     canRetry: false // 初始时不显示重试按钮
420
     canRetry: false // 初始时不显示重试按钮
410
   }
421
   }
411
   chatMessages.value.push(userMessage)
422
   chatMessages.value.push(userMessage)
412
-  
423
+
413
   // 只有在非重试模式下才清空输入框
424
   // 只有在非重试模式下才清空输入框
414
   if (!retryContent) {
425
   if (!retryContent) {
415
     inputMessage.value = ''
426
     inputMessage.value = ''
416
   }
427
   }
417
-  
428
+
418
   isTyping.value = true
429
   isTyping.value = true
419
 
430
 
420
   nextTick(() => {
431
   nextTick(() => {
429
     })
440
     })
430
     let content = JSON.parse(response.resultContent).content;
441
     let content = JSON.parse(response.resultContent).content;
431
     console.log(content);
442
     console.log(content);
432
-    
443
+
433
     // 检查是否是默认的失败回复
444
     // 检查是否是默认的失败回复
434
     const defaultFailureMessage = '抱歉,我暂时无法回答这个问题。';
445
     const defaultFailureMessage = '抱歉,我暂时无法回答这个问题。';
435
-    const finalContent = content || defaultFailureMessage;
436
-    
446
+    const finalContent = formatContentLinks(content) || defaultFailureMessage;
447
+
437
     // 添加助手回复
448
     // 添加助手回复
438
     const assistantMessage = {
449
     const assistantMessage = {
439
       role: 'assistant',
450
       role: 'assistant',
442
       isHtml: true // 普通聊天消息使用HTML渲染
453
       isHtml: true // 普通聊天消息使用HTML渲染
443
     }
454
     }
444
     chatMessages.value.push(assistantMessage)
455
     chatMessages.value.push(assistantMessage)
445
-    
456
+
446
     // 如果是默认失败回复,为用户消息添加重试按钮
457
     // 如果是默认失败回复,为用户消息添加重试按钮
447
     if (finalContent === defaultFailureMessage) {
458
     if (finalContent === defaultFailureMessage) {
448
       userMessage.canRetry = true
459
       userMessage.canRetry = true
459
       isHtml: false
470
       isHtml: false
460
     }
471
     }
461
     chatMessages.value.push(errorMessage)
472
     chatMessages.value.push(errorMessage)
462
-    
473
+
463
     // 为用户消息添加重试按钮
474
     // 为用户消息添加重试按钮
464
     userMessage.canRetry = true
475
     userMessage.canRetry = true
465
   } finally {
476
   } finally {
475
   // 找到当前消息在数组中的索引
486
   // 找到当前消息在数组中的索引
476
   const messageIndex = chatMessages.value.findIndex(msg => msg === message)
487
   const messageIndex = chatMessages.value.findIndex(msg => msg === message)
477
   if (messageIndex === -1) return
488
   if (messageIndex === -1) return
478
-  
489
+
479
   // 获取原始消息内容
490
   // 获取原始消息内容
480
   const originalContent = message.originalContent || message.content
491
   const originalContent = message.originalContent || message.content
481
   if (!originalContent || typeof originalContent !== 'string') {
492
   if (!originalContent || typeof originalContent !== 'string') {
482
     ElMessage.error('无法获取原始消息内容')
493
     ElMessage.error('无法获取原始消息内容')
483
     return
494
     return
484
   }
495
   }
485
-  
496
+
486
   // 移除从当前用户消息开始的所有消息(包括后续的助手回复)
497
   // 移除从当前用户消息开始的所有消息(包括后续的助手回复)
487
   const messagesToKeep = chatMessages.value.slice(0, messageIndex)
498
   const messagesToKeep = chatMessages.value.slice(0, messageIndex)
488
   chatMessages.value = messagesToKeep
499
   chatMessages.value = messagesToKeep
489
-  
500
+
490
   // 重新发送原始消息
501
   // 重新发送原始消息
491
   await sendMessage(originalContent)
502
   await sendMessage(originalContent)
492
 }
503
 }
525
     return
536
     return
526
   }
537
   }
527
   try {
538
   try {
539
+    percentageDisplay.value = true;
540
+    var timer;
541
+    clearInterval(timer);
542
+    getProcess()
543
+    function getProcess() {
544
+      timer = setInterval(function () { //隔2000毫秒获取进度
545
+        getProcessValue().then(res => {
546
+          if (res.code == 200 & res.msg.includes(":")) {
547
+            progress.value = res.msg;
548
+            percentage.value = Number(res.msg.split(":")[1].replace("%", ""))
549
+          }
550
+        });
551
+      }, 2000)
552
+    }
528
     const file = chatFileList.value[0].raw // 只取第一个文件
553
     const file = chatFileList.value[0].raw // 只取第一个文件
529
     const fileName = file.name;
554
     const fileName = file.name;
530
-    const response = await uploadFile(file, agentInfo.value.agentName)
531
-    const chatId = response.data.chatId; //获取保存后的chatId
532
-    // 解析返回的数据
533
-    let assistantContent = '文件上传成功!'
534
-    if (response.data && response.data.assistantMessage) {
535
-      // 格式化链接:在href前加上基础API地址
536
-      assistantContent = formatContentLinks(response.data.assistantMessage)
537
-    }
538
-
539
-    // 添加上传成功的消息到聊天记录
540
-    const uploadMessage = {
541
-      role: 'assistant',
542
-      content: assistantContent,
543
-      timestamp: new Date().toLocaleTimeString(),
544
-      isHtml: true // 标记这是HTML内容
545
-    }
546
-    chatMessages.value.push(uploadMessage);
547
-    ElMessage.success('文件上传成功');
548
-    let topicRes = await addTopic({ agentId: props.agentId, topic: fileName });
549
-    const topicId = topicRes.msg;
550
-    await updateChat({ userId: userStore.id, chatId, topicId, output: assistantContent, outputTime: proxy.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}') });
551
-
552
-    // 刷新话题列表
553
-    await loadTopic();
555
+    try {
556
+      const response = await uploadFile(file, agentInfo.value.agentName)
557
+      const chatId = response.data.chatId; //获取保存后的chatId
558
+      // 解析返回的数据
559
+      if (response.data && response.data.assistantMessage) {
560
+        percentageDisplay.value = false;
561
+        // 格式化链接:在href前加上基础API地址
562
+        let assistantContent = formatContentLinks(response.data.assistantMessage)
563
+        clearInterval(timer);
564
+        // 添加上传成功的消息到聊天记录
565
+        const uploadMessage = {
566
+          role: 'assistant',
567
+          content: assistantContent,
568
+          timestamp: new Date().toLocaleTimeString(),
569
+          isHtml: true // 标记这是HTML内容
570
+        }
571
+        chatMessages.value.push(uploadMessage);
572
+        // ElMessage.success('文件上传成功');
573
+        let topicRes = await addTopic({ agentId: props.agentId, topic: fileName });
574
+        const topicId = topicRes.msg;
575
+        await updateChat({ userId: userStore.id, chatId, topicId, output: assistantContent, outputTime: proxy.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}') });
576
+
577
+        // 刷新话题列表
578
+        await loadTopic();
579
+
580
+        // 激活当前话题并加载聊天记录
581
+        currentTopicId.value = topicId;
582
+        chatTitle.value = fileName;
583
+
584
+        // 重新加载该话题的聊天记录以显示文件相关内容
585
+        try {
586
+          await loadChatMessages(topicId)
587
+        } catch (error) {
588
+          console.error('加载话题聊天记录失败:', error)
589
+        }
554
 
590
 
555
-    // 激活当前话题并加载聊天记录
556
-    currentTopicId.value = topicId;
557
-    chatTitle.value = fileName;
591
+        // 清空文件上传列表
592
+        chatFileList.value = [];
558
 
593
 
559
-    // 重新加载该话题的聊天记录以显示文件相关内容
560
-    try {
561
-      await loadChatMessages(topicId)
562
-    } catch (error) {
563
-      console.error('加载话题聊天记录失败:', error)
594
+        nextTick(() => {
595
+          scrollToBottom()
596
+        })
597
+      }
598
+    }
599
+    catch (error) {
600
+      clearInterval(timer);
601
+      console.error('文件上传失败:', error)
564
     }
602
     }
565
 
603
 
566
-    // 清空文件上传列表
567
-    chatFileList.value = [];
568
-
569
-    nextTick(() => {
570
-      scrollToBottom()
571
-    })
572
   } catch (error) {
604
   } catch (error) {
573
     console.error('文件上传失败:', error)
605
     console.error('文件上传失败:', error)
574
     ElMessage.error('文件上传失败')
606
     ElMessage.error('文件上传失败')
905
       flex: 1;
937
       flex: 1;
906
       padding: 16px 20px;
938
       padding: 16px 20px;
907
       overflow-y: auto;
939
       overflow-y: auto;
908
-      max-height: 475px;
940
+      max-height: 900px;
909
 
941
 
910
       .message-item {
942
       .message-item {
911
         display: flex;
943
         display: flex;
929
               background: #1890ff;
961
               background: #1890ff;
930
               color: white;
962
               color: white;
931
             }
963
             }
932
-            
964
+
933
             .message-actions {
965
             .message-actions {
934
               justify-content: flex-end;
966
               justify-content: flex-end;
935
-              
967
+
936
               .retry-btn {
968
               .retry-btn {
937
                 order: -1; // 将重试按钮放在时间前面
969
                 order: -1; // 将重试按钮放在时间前面
938
                 margin-left: 0;
970
                 margin-left: 0;
939
                 margin-right: 8px;
971
                 margin-right: 8px;
940
                 color: #1890ff;
972
                 color: #1890ff;
941
                 background-color: rgba(255, 255, 255, 0.9);
973
                 background-color: rgba(255, 255, 255, 0.9);
942
-                
974
+
943
                 &:hover {
975
                 &:hover {
944
                   background-color: white;
976
                   background-color: white;
945
                 }
977
                 }
1030
             align-items: center;
1062
             align-items: center;
1031
             justify-content: space-between;
1063
             justify-content: space-between;
1032
             margin-top: 4px;
1064
             margin-top: 4px;
1033
-            
1065
+
1034
             .message-time {
1066
             .message-time {
1035
               font-size: 11px;
1067
               font-size: 11px;
1036
               color: #999;
1068
               color: #999;
1037
             }
1069
             }
1038
-            
1070
+
1039
             .retry-btn {
1071
             .retry-btn {
1040
               font-size: 11px;
1072
               font-size: 11px;
1041
               color: #1890ff;
1073
               color: #1890ff;
1042
               padding: 2px 6px;
1074
               padding: 2px 6px;
1043
               margin-left: 8px;
1075
               margin-left: 8px;
1044
-              
1076
+
1045
               &:hover {
1077
               &:hover {
1046
                 background-color: #f0f8ff;
1078
                 background-color: #f0f8ff;
1047
               }
1079
               }
1048
-              
1080
+
1049
               .el-icon {
1081
               .el-icon {
1050
                 font-size: 12px;
1082
                 font-size: 12px;
1051
                 margin-right: 2px;
1083
                 margin-right: 2px;
1160
     margin-top: 16px;
1192
     margin-top: 16px;
1161
     font-size: 14px;
1193
     font-size: 14px;
1162
   }
1194
   }
1163
-}
1164
-</style>
1195
+}</style>

Loading…
Cancel
Save