lamphua 2 тижднів тому
джерело
коміт
acb273d427

+ 80
- 53
llm-back/ruoyi-agent/src/main/java/com/ruoyi/agent/service/impl/McpServiceImpl.java Переглянути файл

@@ -33,7 +33,6 @@ import org.noear.solon.ai.annotation.ToolMapping;
33 33
 import org.noear.solon.ai.chat.ChatModel;
34 34
 import org.noear.solon.ai.chat.ChatResponse;
35 35
 import org.noear.solon.ai.chat.ChatSession;
36
-import org.noear.solon.ai.chat.ChatSessionDefault;
37 36
 import org.noear.solon.ai.chat.message.AssistantMessage;
38 37
 import org.noear.solon.ai.chat.message.ChatMessage;
39 38
 import org.noear.solon.ai.chat.session.InMemoryChatSession;
@@ -70,10 +69,10 @@ public class McpServiceImpl implements IMcpService {
70 69
     {
71 70
             try {
72 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 73
 //                List<JSONObject> contexts = retrieveFromMilvus(milvusClient, embeddingModel, collectionName, title, 10);
75 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 76
             } catch (IOException e) {
78 77
                 throw new RuntimeException(e);
79 78
             }
@@ -98,7 +97,7 @@ public class McpServiceImpl implements IMcpService {
98 97
     /**
99 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 101
         StringBuilder sb = new StringBuilder("招标文件内容:\n\n");
103 102
         String filename = templatePath.replace("_" + agentName, "");
104 103
         File profilePath = new File(filename);
@@ -110,68 +109,74 @@ public class McpServiceImpl implements IMcpService {
110 109
         List<TextSegment> segments = splitDocument(profilePath);
111 110
         List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
112 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 131
 //        for (JSONObject context : contexts) {
127 132
 //            sb.append("文件").append(": ")
128 133
 //                    .append(context.getString("file_name")).append("\n\n")
129 134
 //                    .append("段落格式").append(": ")
130 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 146
      * 调用LLM生成回答
137 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 150
         ChatModel chatModel = ChatModel.of(llmServiceUrl)
141
-                
142 151
                 .model("Qwen2.5-1.5B-Instruct")
143 152
                 .build();
144 153
 
154
+
145 155
         List<ChatMessage> messages = new ArrayList<>();
146 156
         messages.add(ChatMessage.ofUser(prompt));
147 157
         ChatSession chatSession =  InMemoryChatSession.builder().messages(messages).build();
158
+        chatSession.addMessage(messages);
148 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 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 169
         String[] contentLines = content.split("\n");
164 170
         Map<String, String> map = new HashMap<>();
165
-        String[] titles = question.split(",");
166 171
         File file = new File(absolutePath);
167 172
         FileInputStream fileInputStream = new FileInputStream(file);
168 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 176
                 StringBuilder text = new StringBuilder();
172 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 180
                         for (int c = startIndex + 1; c < endIndex; c++) {
176 181
                             text.append(contentLines[c]).append("\n\n");
177 182
                         }
@@ -185,7 +190,7 @@ public class McpServiceImpl implements IMcpService {
185 190
                 }
186 191
                 else
187 192
                     text.append(content);
188
-                map.put(titles[i], text.toString());
193
+                map.put(titles.get(i), text.toString());
189 194
             }
190 195
 
191 196
             List<Integer> positions = new ArrayList<>();
@@ -218,41 +223,63 @@ public class McpServiceImpl implements IMcpService {
218 223
     }
219 224
 
220 225
     /**
221
-     * 获取二级标题下三级标题列表
226
+     * 获取最低级别子标题列表
222 227
      */
223 228
     public List<String> extractSubTitles(String filename, String question) throws IOException {
224 229
         List<String> subTitles = new ArrayList<>();
225
-        boolean inTargetSection = false;
226 230
         InputStream fileInputStream = new FileInputStream(filename);
231
+
232
+        boolean foundParent = false;
233
+        int parentLevel = -1;
234
+        // 用于跟踪当前路径
235
+        List<XWPFParagraph> currentPath = new ArrayList<>();
236
+
227 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 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 Переглянути файл

@@ -99,6 +99,14 @@ public class CmcAgentController extends BaseController
99 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 Переглянути файл

@@ -61,33 +61,6 @@ public class McpController extends BaseController
61 61
                     .withHost("192.168.28.188")
62 62
                     .withPort(19530)
63 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 66
      * 自动调用mcp工具问答
@@ -125,6 +98,13 @@ public class McpController extends BaseController
125 98
                 arguments.put("collectionName", "technical");
126 99
                 String agentName = cmcAgentService.selectCmcAgentByAgentId(cmcTopicService.selectCmcTopicByTopicId(topicId).getAgentId()).getAgentName();
127 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 109
             resultContent = clientProvider.callToolAsText(name, arguments).getContent();
130 110
             assistantMessage = new AssistantMessage(resultContent);
@@ -134,42 +114,5 @@ public class McpController extends BaseController
134 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 Переглянути файл

@@ -55,6 +55,13 @@ public interface ICmcAgentService
55 55
      */
56 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 Переглянути файл

@@ -1,6 +1,9 @@
1 1
 package com.ruoyi.llm.service.impl;
2 2
 
3 3
 import java.io.*;
4
+import java.nio.file.Files;
5
+import java.nio.file.Path;
6
+import java.nio.file.Paths;
4 7
 import java.util.*;
5 8
 import java.util.stream.Collectors;
6 9
 
@@ -70,6 +73,8 @@ public class CmcAgentServiceImpl implements ICmcAgentService
70 73
     @Autowired
71 74
     private CmcChatMapper cmcChatMapper;
72 75
 
76
+    private String processValue = "";
77
+
73 78
     private static final EmbeddingModel embeddingModel = new BgeSmallZhV15EmbeddingModel();
74 79
 
75 80
     private static final String llmServiceUrl = "http://192.168.28.188:8000/v1/chat/completions";
@@ -136,6 +141,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService
136 141
         File transferFile = new File(profilePath + "/" + file.getOriginalFilename());
137 142
         if (!transferFile.exists())
138 143
             file.transferTo(transferFile);
144
+        processValue = "上传完成:100%";
139 145
         CmcDocument cmcDocument = new CmcDocument();
140 146
         cmcDocument.setDocumentId(new SnowFlake().generateId());
141 147
         cmcDocument.setChatId(chatId);
@@ -154,6 +160,8 @@ public class CmcAgentServiceImpl implements ICmcAgentService
154 160
                     .replace(filenameSplit[filenameSplit.length - 2], filenameSplit[filenameSplit.length - 2] + "_" + agentName);
155 161
             if (file.getOriginalFilename().endsWith(".doc"))
156 162
                 outputFilename = outputFilename.replace(".doc", ".docx");
163
+            Path outputFilePath = Paths.get(RuoYiConfig.getProfile() + outputFilename);
164
+            Files.deleteIfExists(outputFilePath);
157 165
             InputStream fileInputStream = new FileInputStream(RuoYiConfig.getProfile() + "/upload/agent/template/technical.docx");
158 166
             try (XWPFDocument doc = new XWPFDocument(fileInputStream)) {
159 167
                 // 保存文档到本地文件系统
@@ -163,10 +171,11 @@ public class CmcAgentServiceImpl implements ICmcAgentService
163 171
             }
164 172
             String chapters = generateAnswerWithDocument(profilePath + "/" + file.getOriginalFilename(),
165 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 180
         jsonObject.put("assistantMessage", message);
172 181
         return jsonObject;
@@ -191,6 +200,15 @@ public class CmcAgentServiceImpl implements ICmcAgentService
191 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,6 +280,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService
262 280
             String requests = embeddingMatch.embedded().toString();
263 281
             sb.append(requests).append("\n\n");
264 282
         }
283
+        //根据招标文件工作大纲要求生成二级标题
265 284
         sb.append("请根据上述招标文件内容,严格按以下格式列出").append(question).append(":\n")
266 285
                 .append("6.1 XX\n" +
267 286
                 "6.2 XX\n" +
@@ -274,25 +293,42 @@ public class CmcAgentServiceImpl implements ICmcAgentService
274 293
      * @return
275 294
      */
276 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 332
         writeContent(content, templatePath);
297 333
         return content;
298 334
     }
@@ -302,7 +338,9 @@ public class CmcAgentServiceImpl implements ICmcAgentService
302 338
      * @return
303 339
      */
304 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 345
         List<ChatMessage> messages = new ArrayList<>();
308 346
         messages.add(ChatMessage.ofUser(prompt));
@@ -383,15 +421,14 @@ public class CmcAgentServiceImpl implements ICmcAgentService
383 421
         try (XWPFDocument document = new XWPFDocument(fileInputStream)) {
384 422
            for (int i = 0; i < chapters.size(); i++) {
385 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 427
                XWPFRun run = contentParagraph.createRun();
392
-               if (chapters.get(i).contains(" "))
428
+               if (chapters.get(i).split(" ").length > 1)
393 429
                    run.setText(chapters.get(i).split(" ")[1]);
394
-               run.addBreak();
430
+               if (dotNum > 1)
431
+                   run.addBreak();
395 432
                if (i < chapters.size() - 1 && chapters.get(i + 1).indexOf(".") == chapters.get(i + 1).lastIndexOf("."))
396 433
                    run.addBreak(BreakType.PAGE);
397 434
            }
@@ -403,7 +440,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService
403 440
     }
404 441
 
405 442
     /**
406
-     * 检索知识库
443
+     * 分割文档
407 444
      */
408 445
     private List<TextSegment> splitDocument(File transferFile) throws IOException {
409 446
         // 加载文档

+ 8
- 0
llm-ui/src/api/llm/agent.js Переглянути файл

@@ -81,6 +81,14 @@ export function uploadFileList(fileList, agentName) {
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 92
 //获取开场白, agentName:智能体名称
85 93
 export function opening(agentName) {
86 94
   return request({

+ 94
- 63
llm-ui/src/views/llm/agent/AgentDetail.vue Переглянути файл

@@ -1,8 +1,8 @@
1 1
 <!--
2 2
  * @Author: wrh
3 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 7
 <template>
8 8
   <div class="agent-detail-container" v-loading="loading">
@@ -100,6 +100,11 @@
100 100
                       确认上传
101 101
                     </el-button>
102 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 108
                 </div>
104 109
               </div>
105 110
             </div>
@@ -122,8 +127,11 @@
122 127
                 <div v-else class="message-text">{{ message.content }}</div>
123 128
                 <div class="message-actions">
124 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 136
                   </el-button>
129 137
                 </div>
@@ -172,7 +180,7 @@
172 180
 import { ref, reactive, watch, nextTick, computed, getCurrentInstance } from 'vue';
173 181
 import { ElMessage, ElMessageBox } from 'element-plus';
174 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 184
 import { answer } from '@/api/llm/mcp';
177 185
 import { listTopic, getTopic, delTopic, addTopic, updateTopic } from "@/api/llm/topic";
178 186
 import { listChat, addChat, updateChat } from "@/api/llm/chat";
@@ -202,6 +210,9 @@ const messagesContainer = ref(null)
202 210
 const topicList = ref([])
203 211
 const currentTopicId = ref(null)
204 212
 const chatTitle = ref('智能体新对话')
213
+const percentage = ref(0);
214
+const progress = ref("");
215
+const percentageDisplay = ref(false);
205 216
 
206 217
 // 聊天内文件上传相关
207 218
 const chatFileList = ref([]) // 聊天内的文件列表
@@ -409,12 +420,12 @@ const sendMessage = async (retryContent = null) => {
409 420
     canRetry: false // 初始时不显示重试按钮
410 421
   }
411 422
   chatMessages.value.push(userMessage)
412
-  
423
+
413 424
   // 只有在非重试模式下才清空输入框
414 425
   if (!retryContent) {
415 426
     inputMessage.value = ''
416 427
   }
417
-  
428
+
418 429
   isTyping.value = true
419 430
 
420 431
   nextTick(() => {
@@ -429,11 +440,11 @@ const sendMessage = async (retryContent = null) => {
429 440
     })
430 441
     let content = JSON.parse(response.resultContent).content;
431 442
     console.log(content);
432
-    
443
+
433 444
     // 检查是否是默认的失败回复
434 445
     const defaultFailureMessage = '抱歉,我暂时无法回答这个问题。';
435
-    const finalContent = content || defaultFailureMessage;
436
-    
446
+    const finalContent = formatContentLinks(content) || defaultFailureMessage;
447
+
437 448
     // 添加助手回复
438 449
     const assistantMessage = {
439 450
       role: 'assistant',
@@ -442,7 +453,7 @@ const sendMessage = async (retryContent = null) => {
442 453
       isHtml: true // 普通聊天消息使用HTML渲染
443 454
     }
444 455
     chatMessages.value.push(assistantMessage)
445
-    
456
+
446 457
     // 如果是默认失败回复,为用户消息添加重试按钮
447 458
     if (finalContent === defaultFailureMessage) {
448 459
       userMessage.canRetry = true
@@ -459,7 +470,7 @@ const sendMessage = async (retryContent = null) => {
459 470
       isHtml: false
460 471
     }
461 472
     chatMessages.value.push(errorMessage)
462
-    
473
+
463 474
     // 为用户消息添加重试按钮
464 475
     userMessage.canRetry = true
465 476
   } finally {
@@ -475,18 +486,18 @@ const retryMessage = async (message) => {
475 486
   // 找到当前消息在数组中的索引
476 487
   const messageIndex = chatMessages.value.findIndex(msg => msg === message)
477 488
   if (messageIndex === -1) return
478
-  
489
+
479 490
   // 获取原始消息内容
480 491
   const originalContent = message.originalContent || message.content
481 492
   if (!originalContent || typeof originalContent !== 'string') {
482 493
     ElMessage.error('无法获取原始消息内容')
483 494
     return
484 495
   }
485
-  
496
+
486 497
   // 移除从当前用户消息开始的所有消息(包括后续的助手回复)
487 498
   const messagesToKeep = chatMessages.value.slice(0, messageIndex)
488 499
   chatMessages.value = messagesToKeep
489
-  
500
+
490 501
   // 重新发送原始消息
491 502
   await sendMessage(originalContent)
492 503
 }
@@ -525,50 +536,71 @@ const submitChatUpload = async () => {
525 536
     return
526 537
   }
527 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 553
     const file = chatFileList.value[0].raw // 只取第一个文件
529 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 604
   } catch (error) {
573 605
     console.error('文件上传失败:', error)
574 606
     ElMessage.error('文件上传失败')
@@ -905,7 +937,7 @@ const formatContentLinks = (content) => {
905 937
       flex: 1;
906 938
       padding: 16px 20px;
907 939
       overflow-y: auto;
908
-      max-height: 475px;
940
+      max-height: 900px;
909 941
 
910 942
       .message-item {
911 943
         display: flex;
@@ -929,17 +961,17 @@ const formatContentLinks = (content) => {
929 961
               background: #1890ff;
930 962
               color: white;
931 963
             }
932
-            
964
+
933 965
             .message-actions {
934 966
               justify-content: flex-end;
935
-              
967
+
936 968
               .retry-btn {
937 969
                 order: -1; // 将重试按钮放在时间前面
938 970
                 margin-left: 0;
939 971
                 margin-right: 8px;
940 972
                 color: #1890ff;
941 973
                 background-color: rgba(255, 255, 255, 0.9);
942
-                
974
+
943 975
                 &:hover {
944 976
                   background-color: white;
945 977
                 }
@@ -1030,22 +1062,22 @@ const formatContentLinks = (content) => {
1030 1062
             align-items: center;
1031 1063
             justify-content: space-between;
1032 1064
             margin-top: 4px;
1033
-            
1065
+
1034 1066
             .message-time {
1035 1067
               font-size: 11px;
1036 1068
               color: #999;
1037 1069
             }
1038
-            
1070
+
1039 1071
             .retry-btn {
1040 1072
               font-size: 11px;
1041 1073
               color: #1890ff;
1042 1074
               padding: 2px 6px;
1043 1075
               margin-left: 8px;
1044
-              
1076
+
1045 1077
               &:hover {
1046 1078
                 background-color: #f0f8ff;
1047 1079
               }
1048
-              
1080
+
1049 1081
               .el-icon {
1050 1082
                 font-size: 12px;
1051 1083
                 margin-right: 2px;
@@ -1160,5 +1192,4 @@ const formatContentLinks = (content) => {
1160 1192
     margin-top: 16px;
1161 1193
     font-size: 14px;
1162 1194
   }
1163
-}
1164
-</style>
1195
+}</style>

Завантаження…
Відмінити
Зберегти