lamphua 1 день назад
Родитель
Сommit
8f6a68fe76

+ 5
- 7
oa-back/ruoyi-llm/src/main/java/com/ruoyi/web/llm/controller/CmcAgentController.java Просмотреть файл

@@ -1,9 +1,7 @@
1 1
 package com.ruoyi.web.llm.controller;
2 2
 
3
-import java.io.IOException;
4 3
 import java.util.Date;
5 4
 import java.util.List;
6
-import java.util.concurrent.ExecutionException;
7 5
 
8 6
 import javax.servlet.http.HttpServletResponse;
9 7
 
@@ -88,7 +86,7 @@ public class CmcAgentController extends BaseController
88 86
      * 保存目录到Word文件
89 87
      */
90 88
     @PostMapping("/writeTitles")
91
-    public AjaxResult writeTitles(@RequestBody JSONObject data) throws IOException
89
+    public AjaxResult writeTitles(@RequestBody JSONObject data) throws Throwable
92 90
     {
93 91
         JSONObject result = cmcAgentService.writeTitles(data);
94 92
         if (result.getIntValue("code") == 200) {
@@ -103,7 +101,7 @@ public class CmcAgentController extends BaseController
103 101
      * @return
104 102
      */
105 103
     @PostMapping("/upload")
106
-    public AjaxResult upload(MultipartFile file, String agentName) throws IOException
104
+    public AjaxResult upload(MultipartFile file, String agentName) throws Throwable
107 105
     {
108 106
         return success(cmcAgentService.uploadDocument(file, agentName));
109 107
     }
@@ -113,7 +111,7 @@ public class CmcAgentController extends BaseController
113 111
      * @return
114 112
      */
115 113
     @PostMapping("/modifyFile")
116
-    public AjaxResult uploadModifyFile(String topicId, MultipartFile file, String agentName, String selectedNode) throws IOException, InterruptedException, ExecutionException
114
+    public AjaxResult uploadModifyFile(String topicId, MultipartFile file, String agentName, String selectedNode) throws Throwable
117 115
     {
118 116
         return success(cmcAgentService.uploadModifyFile(topicId, file, agentName, selectedNode));
119 117
     }
@@ -123,7 +121,7 @@ public class CmcAgentController extends BaseController
123 121
      */
124 122
     @Anonymous
125 123
     @PostMapping(value = "/streamGenerate", produces = "text/event-stream;charset=UTF-8")
126
-    public SseEmitter streamGenerateChapters(String topicId, MultipartFile file, String agentName, String selectedNode) throws IOException
124
+    public SseEmitter streamGenerateChapters(String topicId, MultipartFile file, String agentName, String selectedNode) throws Throwable
127 125
     {
128 126
         return cmcAgentService.streamGenerateChapters(topicId, file, agentName, selectedNode);
129 127
     }
@@ -133,7 +131,7 @@ public class CmcAgentController extends BaseController
133 131
      * @return
134 132
      */
135 133
     @PostMapping("/uploadList")
136
-    public AjaxResult uploadList(MultipartFile[] fileList, String agentName) throws IOException
134
+    public AjaxResult uploadList(MultipartFile[] fileList, String agentName) throws Throwable
137 135
     {
138 136
         return success(cmcAgentService.uploadDocumentList(fileList, agentName));
139 137
     }

+ 7
- 1
oa-back/ruoyi-system/pom.xml Просмотреть файл

@@ -60,7 +60,13 @@
60 60
         <dependency>
61 61
             <groupId>org.noear</groupId>
62 62
             <artifactId>solon-ai</artifactId>
63
-            <version>3.9.5</version>
63
+            <version>4.0.2</version>
64
+        </dependency>
65
+
66
+        <dependency>
67
+            <groupId>org.noear</groupId>
68
+            <artifactId>solon-ai-agent</artifactId>
69
+            <version>4.0.2</version>
64 70
         </dependency>
65 71
 
66 72
         <!-- Spring Boot Web -->

+ 5
- 6
oa-back/ruoyi-system/src/main/java/com/ruoyi/llm/service/ICmcAgentService.java Просмотреть файл

@@ -6,7 +6,6 @@ import com.ruoyi.llm.domain.CmcAgent;
6 6
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
7 7
 import org.springframework.web.multipart.MultipartFile;
8 8
 
9
-import java.io.IOException;
10 9
 import java.util.List;
11 10
 
12 11
 /**
@@ -47,7 +46,7 @@ public interface ICmcAgentService
47 46
      * @param file 文件
48 47
      * @return 结果
49 48
      */
50
-    public JSONObject uploadDocument(MultipartFile file, String agentName) throws IOException;
49
+    public JSONObject uploadDocument(MultipartFile file, String agentName) throws Throwable;
51 50
 
52 51
     /**
53 52
      * 上传修改文件
@@ -58,7 +57,7 @@ public interface ICmcAgentService
58 57
      * @param selectedNode 选中的节点标题(可选,为空时生成全部章节)
59 58
      * @return 结果
60 59
      */
61
-    public JSONObject uploadModifyFile(String topicId, MultipartFile file, String agentName, String selectedNode) throws IOException, InterruptedException, java.util.concurrent.ExecutionException;
60
+    public JSONObject uploadModifyFile(String topicId, MultipartFile file, String agentName, String selectedNode) throws Throwable;
62 61
 
63 62
     /**
64 63
      * 上传多文件
@@ -66,7 +65,7 @@ public interface ICmcAgentService
66 65
      * @param fileList 文件
67 66
      * @return 结果
68 67
      */
69
-    public String uploadDocumentList(MultipartFile[] fileList, String agentName) throws IOException;
68
+    public String uploadDocumentList(MultipartFile[] fileList, String agentName) throws Throwable;
70 69
 
71 70
     /**
72 71
      * 获取进度
@@ -113,7 +112,7 @@ public interface ICmcAgentService
113 112
      * @param data 目录数据
114 113
      * @return 结果
115 114
      */
116
-    public JSONObject writeTitles(JSONObject data) throws IOException;
115
+    public JSONObject writeTitles(JSONObject data) throws Throwable;
117 116
 
118 117
     /**
119 118
      * 流式生成章节内容(SSE方式)
@@ -124,6 +123,6 @@ public interface ICmcAgentService
124 123
      * @param selectedNode 选中的节点标题(可选,为空时生成全部章节)
125 124
      * @return SseEmitter
126 125
      */
127
-    public SseEmitter streamGenerateChapters(String topicId, MultipartFile file, String agentName, String selectedNode) throws IOException;
126
+    public SseEmitter streamGenerateChapters(String topicId, MultipartFile file, String agentName, String selectedNode) throws Throwable;
128 127
 
129 128
 }

+ 154
- 145
oa-back/ruoyi-system/src/main/java/com/ruoyi/llm/service/impl/CmcAgentServiceImpl.java Просмотреть файл

@@ -42,8 +42,8 @@ import org.apache.poi.hwpf.usermodel.Range;
42 42
 import org.apache.poi.xwpf.usermodel.*;
43 43
 import org.apache.xmlbeans.XmlCursor;
44 44
 import org.noear.solon.ai.AiUsage;
45
+import org.noear.solon.ai.agent.simple.SimpleAgent;
45 46
 import org.noear.solon.ai.chat.ChatModel;
46
-import org.noear.solon.ai.chat.ChatResponse;
47 47
 import org.noear.solon.ai.chat.ChatSession;
48 48
 import org.noear.solon.ai.chat.message.ChatMessage;
49 49
 import org.noear.solon.ai.chat.prompt.Prompt;
@@ -138,7 +138,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
138 138
     @Override
139 139
     public String getOpening(String agentName) {
140 140
         String content = "";
141
-        if (agentName.contains("技术"))
141
+        if (agentName.contains("技术标书"))
142 142
             content = "我是投标文件写作助手,我将助您完成技术文件部分撰写。请上传招标询价文件,分析后将依招标服务要求,运用技术方案知识库,为您提供参考。";
143 143
         else if (agentName.contains("检查"))
144 144
             content = "我是文档检查助手,我将助您完成错别字检查。请上传文件。";
@@ -152,7 +152,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
152 152
      * @return 结果
153 153
      */
154 154
     @Override
155
-    public JSONObject uploadDocument(MultipartFile file, String agentName) throws IOException {
155
+    public JSONObject uploadDocument(MultipartFile file, String agentName) throws Throwable {
156 156
         processValue = "";
157 157
         String prefixPath = "/upload/agent/" + agentName;
158 158
         File profilePath = new File(RuoYiConfig.getProfile() + prefixPath);
@@ -176,7 +176,10 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
176 176
         CmcChat cmcChat = new CmcChat();
177 177
         cmcChat.setChatId(jsonObject.getString("chatId"));
178 178
         cmcChat.setInputTime(new Date());
179
-        cmcChat.setInput("招标文件地址:" + prefixPath + "/" + file.getOriginalFilename());
179
+        if (agentName.contains("技术设计书"))
180
+            cmcChat.setInput("项目任务书地址:" + prefixPath + "/" + file.getOriginalFilename());
181
+        else
182
+            cmcChat.setInput("招标文件地址:" + prefixPath + "/" + file.getOriginalFilename());
180 183
         cmcChat.setUserId(SecurityUtils.getUserId());
181 184
         cmcChatMapper.insertCmcChat(cmcChat);
182 185
         if (agentName.contains("技术标书")) {
@@ -197,7 +200,9 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
197 200
             jsonObject.put("projectOverview", projectOverview);
198 201
 
199 202
             // 分析评分标准
200
-            String scoringRequirements = analyzeScoringRequirements(segments, agentName);
203
+            String scoringRequirements = "";
204
+            if (agentName.contains("技术标书"))
205
+                scoringRequirements = analyzeScoringRequirements(segments, agentName);
201 206
             jsonObject.put("scoringRequirements", scoringRequirements);
202 207
 
203 208
             // 提取输出文件名(不含路径)
@@ -209,10 +214,16 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
209 214
             jsonObject.put("detailedDirectory", detailedDirectoryTree);
210 215
             jsonObject.put("directoryText", detailedDirectory);
211 216
 
212
-            message = "好的,我已经收到您上传的招标文件。以下为根据招标文件生成的章节大纲:\n\n" + detailedDirectory + "\n\n" +
213
-                    "若您对章节标题有异议,请打开" + "【<a href='/profile" + outputFilename + "'> 技术文件 " + "</a>】"
214
-                    + "进行修改,后续将根据修改后的章节标题,帮您生成对应章节内容。\n\n" +
215
-                    "思考时间可能较长,请耐心等待!\n";
217
+            if (agentName.contains("技术标书"))
218
+                message = "好的,我已经收到您上传的招标文件。以下为根据招标文件生成的章节大纲:\n\n" + detailedDirectory + "\n\n" +
219
+                        "若您对章节标题有异议,请打开" + "【<a href='/profile" + outputFilename + "'> 技术文件 " + "</a>】"
220
+                        + "进行修改,后续将根据修改后的章节标题,帮您生成对应章节内容。\n\n" +
221
+                        "思考时间可能较长,请耐心等待!\n";
222
+            else
223
+                message = "好的,我已经收到您上传的项目任务书。以下为根据项目任务书生成的章节大纲:\n\n" + detailedDirectory + "\n\n" +
224
+                        "若您对章节标题有异议,请打开" + "【<a href='/profile" + outputFilename + "'> 项目任务书 " + "</a>】"
225
+                        + "进行修改,后续将根据修改后的章节标题,帮您生成对应章节内容。\n\n" +
226
+                        "思考时间可能较长,请耐心等待!\n";
216 227
 
217 228
             // 返回输出文件名,以便前端在保存目录时使用
218 229
             jsonObject.put("filename", file.getOriginalFilename()
@@ -244,7 +255,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
244 255
             StringBuilder output = new StringBuilder();
245 256
             output.append("【项目概况】\n").append(projectOverview).append("\n\n");
246 257
             // 当agentname包含"技术"且包含"询价"时,使用"应价人须知",否则使用"评分标准"
247
-            String scoringTitle = agentName.contains("技术") && agentName.contains("询价") ? "【应价人须知】" : "【评分标准】";
258
+            String scoringTitle = agentName.contains("技术标书") && agentName.contains("询价") ? "【应价人须知】" : "【评分标准】";
248 259
             output.append(scoringTitle).append("\n").append(scoringRequirements).append("\n\n");
249 260
             output.append("【详细目录】\n").append(detailedDirectory);
250 261
 
@@ -269,7 +280,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
269 280
      */
270 281
     @Override
271 282
     public JSONObject uploadModifyFile(String topicId, MultipartFile file, String agentName, String selectedNode)
272
-            throws IOException, InterruptedException, ExecutionException {
283
+            throws IOException, Throwable {
273 284
         processValue = "";
274 285
         String prefixPath = "/upload/agent/" + agentName;
275 286
         File profilePath = new File(RuoYiConfig.getProfile() + prefixPath);
@@ -332,7 +343,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
332 343
         }
333 344
 
334 345
         String message = "";
335
-        if (agentName.contains("技术")) {
346
+        if (agentName.contains("技术标书")) {
336 347
             String docPath = profilePath + "/" + file.getOriginalFilename();
337 348
             // 优先从 Word 文档提取段落 text 格式的叶子标题
338 349
             List<String> allTitles = extractSubTitles(docPath, "技术文件");
@@ -463,7 +474,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
463 474
      */
464 475
     private Map<String, String> generateChaptersContent(String inputPath, String templatePath,
465 476
             List<String> targetTitles, String projectOverview, String scoringRequirements, String agentName)
466
-            throws IOException, InterruptedException, ExecutionException {
477
+            throws Throwable {
467 478
         // 构建招标文件内容的嵌入向量存储(inputPath 是招标文件路径,通过 topicId 查询得到)
468 479
         File tenderFile = new File(inputPath);
469 480
         InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
@@ -527,7 +538,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
527 538
                         titleContentMap.put(title, chapterContent);
528 539
                         successTitles.add(title);
529 540
 
530
-                    } catch (Exception e) {
541
+                    } catch (Throwable e) {
531 542
                         failureTitles.add(title);
532 543
                         System.err.println("生成章节 " + title + " 内容时出错: " + e.getMessage());
533 544
                         titleContentMap.put(title, "该章节内容生成失败,请手动填写。");
@@ -633,7 +644,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
633 644
     /**
634 645
      * 调用LLM生成回答 - 检查文档内容
635 646
      */
636
-    public String generateAnswerWithDocumentContent(String documentPath) throws IOException {
647
+    public String generateAnswerWithDocumentContent(String documentPath) throws Throwable {
637 648
         File profilePath = new File(documentPath);
638 649
         List<TextSegment> segments = splitDocument(profilePath, 10000, 0);
639 650
         StringBuilder sb = new StringBuilder();
@@ -695,8 +706,9 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
695 706
 
696 707
         // 评分标准/应价人须知
697 708
         if (scoringRequirements != null && !scoringRequirements.isEmpty()) {
698
-            // 当agentname包含"技术"且包含"询价"时,使用"应价人须知",否则使用"评分标准"
699
-            String scoringTitle = agentName != null && agentName.contains("技术") && agentName.contains("询价") ? "【应价人须知】"
709
+            // 当agentname包含"技术标书"且包含"询价"时,使用"应价人须知",否则使用"评分标准"
710
+            String scoringTitle = agentName != null && agentName.contains("技术标书") && agentName.contains("询价")
711
+                    ? "【应价人须知】"
700 712
                     : "【评分标准】";
701 713
             prompt.append(scoringTitle).append(":\n");
702 714
             prompt.append(scoringRequirements).append("\n\n");
@@ -735,7 +747,12 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
735 747
         prompt.append("【输出格式】:\n");
736 748
         prompt.append("请直接输出章节的正文内容(不要包含三级标题),正文内容为总-分结构,别写成总-分-总结构。");
737 749
         prompt.append("【标题编号规范】:\n");
738
-        prompt.append("如果需要在章节内添加子标题,必须严格遵循层级编号格式,最多到第六级标题(如6.1,6.1.1,6.1.1.1,6.1.1.1.1,6.1.1.1.1.1,6.1.1.1.1.1.1),且子标题后应添加换行符。\n");
750
+        if (agentName.contains("技术设计书"))
751
+            prompt.append(
752
+                    "如果需要在章节内添加子标题,必须严格遵循层级编号格式,最多到第六级标题(如1.1,1.1.1,1.1.1.1,1.1.1.1.1,1.1.1.1.1.1,1.1.1.1.1.1),且子标题后应添加换行符。\\n");
753
+        else
754
+            prompt.append(
755
+                    "如果需要在章节内添加子标题,必须严格遵循层级编号格式,最多到第六级标题(如6.1,6.1.1,6.1.1.1,6.1.1.1.1,6.1.1.1.1.1,6.1.1.1.1.1.1),且子标题后应添加换行符。\n");
739 756
         prompt.append("各级标题下,正文文字叙述部份的分点说明序号分为六级,\n");
740 757
         prompt.append("第一级按阿拉伯数字序号编排,如1,2,3\n");
741 758
         prompt.append("第二级按带右圆括号的阿拉伯数字序号编排,如1),2),3)\n");
@@ -864,33 +881,52 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
864 881
      * 调用LLM生成目录结构
865 882
      */
866 883
     public JSONArray generateDetailedDirectory(String fileName, String agentName, List<TextSegment> segments)
867
-            throws IOException {
868
-        StringBuilder sb = new StringBuilder("招标文件内容:\n\n");
869
-        InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
870
-        List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
871
-        embeddingStore.addAll(embeddings, segments);
872
-        Embedding queryEmbedding = embeddingModel.embed("技术文件工作大纲要求").content();
873
-        EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
874
-                .queryEmbedding(queryEmbedding)
875
-                .maxResults(5)
876
-                .minScore(0.7)
877
-                .build();
878
-        List<EmbeddingMatch<TextSegment>> results = embeddingStore.search(embeddingSearchRequest).matches();
879
-        results.sort(Comparator.comparingDouble(EmbeddingMatch<TextSegment>::score).reversed());
880
-        for (EmbeddingMatch<TextSegment> embeddingMatch : results) {
881
-            String requests = embeddingMatch.embedded().toString();
882
-            sb.append(requests).append("\n\n");
884
+            throws Throwable {
885
+        StringBuilder sb = new StringBuilder("");
886
+        if (agentName.contains("技术设计书")) {
887
+            sb.append("项目设计书内容:\n\n");
888
+            for (TextSegment segment : segments) {
889
+                sb.append(segment.toString()).append("\n\n");
890
+            }
891
+        } else if (agentName.contains("技术标书")) {
892
+            sb.append("招标文件内容:\n\n");
893
+
894
+            InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
895
+            List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
896
+            embeddingStore.addAll(embeddings, segments);
897
+            Embedding queryEmbedding = embeddingModel.embed("技术文件工作大纲要求").content();
898
+            EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
899
+                    .queryEmbedding(queryEmbedding)
900
+                    .maxResults(5)
901
+                    .minScore(0.7)
902
+                    .build();
903
+            List<EmbeddingMatch<TextSegment>> results = embeddingStore.search(embeddingSearchRequest).matches();
904
+            results.sort(Comparator.comparingDouble(EmbeddingMatch<TextSegment>::score).reversed());
905
+            for (EmbeddingMatch<TextSegment> embeddingMatch : results) {
906
+                String requests = embeddingMatch.embedded().toString();
907
+                sb.append(requests).append("\n\n");
908
+            }
883 909
         }
884 910
         // 根据招标文件工作大纲要求生成二级标题
885
-        sb.append("请基于上述招标文件中提到的")
886
-                .append("技术文件工作大纲要求")
887
-                .append(",先列出二级章节标题,严格按以下格式,仅输出标题列表:\n")
888
-                .append("6.1 XX\n" +
889
-                        "6.2 XX\n" +
890
-                        "6.3 XX\n" +
891
-                        "......\n" +
892
-                        "6.n-1 XX\n" +
893
-                        "6.n XX\n");
911
+        if (agentName.contains("技术标书"))
912
+            sb.append("请基于上述招标文件中提到的")
913
+                    .append("技术文件工作大纲要求")
914
+                    .append(",先列出二级章节标题,严格按以下格式,仅输出标题列表:\n")
915
+                    .append("6.1 XX\n" +
916
+                            "6.2 XX\n" +
917
+                            "6.3 XX\n" +
918
+                            "......\n" +
919
+                            "6.n-1 XX\n" +
920
+                            "6.n XX\n");
921
+        else if (agentName.contains("技术设计书"))
922
+            sb.append("请基于上述项目设计书内容")
923
+                    .append(",先列出二级章节标题,严格按以下格式,仅输出标题列表:\n")
924
+                    .append("1.1 XX\n" +
925
+                            "1.2 XX\n" +
926
+                            "1.3 XX\n" +
927
+                            "......\n" +
928
+                            "1.n-1 XX\n" +
929
+                            "1.n XX\n");
894 930
         String directoryContent = writeChapters(sb.toString(), fileName, agentName);
895 931
         return parseDirectoryToTree(directoryContent);
896 932
     }
@@ -907,7 +943,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
907 943
         String[] lines = directoryContent.split("\n");
908 944
         for (String line : lines) {
909 945
             line = line.trim().replace("*", "").replace("#", "").replace("-", "");
910
-            if (!line.contains("6.") || line.isEmpty()) {
946
+            if ((!line.startsWith("6.") && !line.startsWith("1.")) || line.isEmpty()) {
911 947
                 continue;
912 948
             }
913 949
 
@@ -992,7 +1028,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
992 1028
      * @return 分析结果
993 1029
      */
994 1030
     private String analyzeDocumentContent(List<TextSegment> segments, String query, String analysisPrompt,
995
-            String progressStartMsg, String progressFoundMsg, String progressEndMsg) throws IOException {
1031
+            String progressStartMsg, String progressFoundMsg, String progressEndMsg) throws Throwable {
996 1032
         processValue = progressStartMsg;
997 1033
         StringBuilder sb = new StringBuilder("招标文件内容:\n\n");
998 1034
         InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
@@ -1026,7 +1062,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
1026 1062
     /**
1027 1063
      * 分析项目概况
1028 1064
      */
1029
-    public String analyzeProjectOverview(String uploadFilePath, List<TextSegment> segments) throws IOException {
1065
+    public String analyzeProjectOverview(String uploadFilePath, List<TextSegment> segments) throws Throwable {
1030 1066
         String query = uploadFilePath.split("/")[uploadFilePath.split("/").length - 1] + "项目概况";
1031 1067
         String analysisPrompt = "请基于上述招标文件内容,分析项目概况。\n" +
1032 1068
                 "请以清晰的结构输出分析结果。\n";
@@ -1040,13 +1076,13 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
1040 1076
      * 分析评分标准/应价人须知
1041 1077
      */
1042 1078
     public String analyzeScoringRequirements(List<TextSegment> segments, String agentName)
1043
-            throws IOException {
1079
+            throws Throwable {
1044 1080
         String query = "评分标准、评标办法、打分规则";
1045 1081
         String analysisPrompt = "请基于上述招标文件内容,分析评分标准。\n" +
1046 1082
                 "请以清晰的结构输出分析结果,包括各项的具体分值和评分细则。\n";
1047 1083
 
1048
-        // 当agentname包含"技术"且包含"询价"时,使用"应价人须知"相关查询
1049
-        if (agentName.contains("技术") && agentName.contains("询价")) {
1084
+        // 当agentname包含"技术标书"且包含"询价"时,使用"应价人须知"相关查询
1085
+        if (agentName.contains("技术标书") && agentName.contains("询价")) {
1050 1086
             query = "应价人须知、报价要求、响应要求、投标报价";
1051 1087
             analysisPrompt = "请基于上述招标文件内容,分析应价人须知。\n" +
1052 1088
                     "请以清晰的结构输出分析结果,包括报价要求、响应要求等内容。\n";
@@ -1054,7 +1090,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
1054 1090
                     "分析应价人须知中: 0%",
1055 1091
                     "已查到应价人须知: 50%",
1056 1092
                     "分析应价人须知中: 100%");
1057
-        } else if (agentName.contains("技术")) {
1093
+        } else if (agentName.contains("技术标书")) {
1058 1094
             query = "技术部分评分标准、评标办法、打分规则";
1059 1095
         } else if (agentName.contains("商务")) {
1060 1096
             query = "商务部分评分标准、评标办法、打分规则";
@@ -1071,7 +1107,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
1071 1107
      * 
1072 1108
      * @return
1073 1109
      */
1074
-    public String writeChapters(String prompt, String fileName, String agentName) throws IOException {
1110
+    public String writeChapters(String prompt, String fileName, String agentName) throws Throwable {
1075 1111
         String chapters2 = generateAnswer(prompt);
1076 1112
 
1077 1113
         // 根据技术文档知识库生成二级标题下三级标题
@@ -1079,7 +1115,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
1079 1115
         List<String> chapter2List = new ArrayList<>();
1080 1116
         String[] contentLines = chapters2.split("\n");
1081 1117
         for (String line : contentLines) {
1082
-            if (line.contains("6."))
1118
+            if (line.startsWith("6.") || line.startsWith("1."))
1083 1119
                 chapter2List.add(line.replace("*", "").replace("#", "").replace("-", ""));
1084 1120
         }
1085 1121
         processValue = "已生成二级标题:100%";
@@ -1103,90 +1139,61 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
1103 1139
         }
1104 1140
         String sb = "二级标题如下:\n" + chapters2 +
1105 1141
                 "\n 三级标题如下:" + chapter3 +
1106
-                "请将上述二级与对应的三级标题进行合并,严格按以下格式输出完整大纲,仅输出标题:\n" +
1107
-                "6 技术文件\n" +
1108
-                "6.1 XX\n" +
1109
-                "6.1.1 XX\n" +
1110
-                "6.1.2 XX\n" +
1111
-                "6.2 XX\n" +
1112
-                "6.2.1 XX\n" +
1113
-                "6.2.2 XX\n" +
1114
-                "6.3 XX\n" +
1115
-                "6.3.1 XX\n" +
1116
-                "6.3.2 XX\n" +
1117
-                "...... \n" +
1118
-                "6.n XX\n" +
1119
-                "6.n.1 XX\n" +
1120
-                "6.n 2 XX\n";
1142
+                "请将上述二级与对应的三级标题进行合并,严格按以下格式输出完整大纲,仅输出标题:\n";
1143
+        if (agentName.contains("技术标书"))
1144
+            sb += "6 技术文件\n" +
1145
+                    "6.1 XX\n" +
1146
+                    "6.1.1 XX\n" +
1147
+                    "6.1.2 XX\n" +
1148
+                    "6.2 XX\n" +
1149
+                    "6.2.1 XX\n" +
1150
+                    "6.2.2 XX\n" +
1151
+                    "6.3 XX\n" +
1152
+                    "6.3.1 XX\n" +
1153
+                    "6.3.2 XX\n" +
1154
+                    "...... \n" +
1155
+                    "6.n XX\n" +
1156
+                    "6.n.1 XX\n" +
1157
+                    "6.n 2 XX\n";
1158
+        if (agentName.contains("技术设计书"))
1159
+            sb += "1 技术设计书\n" +
1160
+                    "1.1 XX\n" +
1161
+                    "1.1.1 XX\n" +
1162
+                    "1.1.2 XX\n" +
1163
+                    "1.2 XX\n" +
1164
+                    "1.2.1 XX\n" +
1165
+                    "1.2.2 XX\n" +
1166
+                    "1.3 XX\n" +
1167
+                    "1.3.1 XX\n" +
1168
+                    "1.3.2 XX\n" +
1169
+                    "...... \n" +
1170
+                    "1.n XX\n" +
1171
+                    "1.n.1 XX\n" +
1172
+                    "1.n 2 XX\n";
1121 1173
         String content = generateAnswer(sb);
1122 1174
         writeTitles(content, fileName, agentName);
1123 1175
         return content;
1124 1176
     }
1125 1177
 
1126
-    // /**
1127
-    // * 调用LLM生成回答
1128
-    // *
1129
-    // * @return
1130
-    // */
1131
-    // public String generateAnswer(String prompt) throws IOException {
1132
-    // ChatModel chatModel =
1133
-    // ChatModel.of("https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions")
1134
-    // .model("qwen3-vl-32b-instruct")
1135
-    // .apiKey("sk-750a17cc723847f28b31fa1bc17c255c")
1136
-    // .timeout(Duration.ofSeconds(240))
1137
-    // .build();
1138
-
1139
-    // List<ChatMessage> messages = new ArrayList<>();
1140
-    // messages.add(ChatMessage.ofUser(prompt));
1141
-    // ChatSession chatSession =
1142
-    // InMemoryChatSession.builder().messages(messages).build();
1143
-
1144
-    // Prompt prompt1 = Prompt.of(prompt).attrPut("session", chatSession);
1145
-    // ChatResponse response = chatModel.prompt(prompt1).call();
1146
-    // String content = response.lastChoice().getMessage().getContent();
1147
-    // return content;
1148
-    // }
1149
-
1150 1178
     /**
1151 1179
      * 调用LLM生成回答
1152 1180
      *
1153 1181
      * @return
1154 1182
      */
1155
-    public String generateAnswer(String prompt) throws IOException {
1183
+    public String generateAnswer(String prompt) throws Throwable, IOException {
1156 1184
         ChatModel chatModel = ChatModel.of(llmServiceUrl)
1157 1185
                 .model("Qwen")
1158 1186
                 .timeout(Duration.ofSeconds(240))
1159 1187
                 .build();
1160
-
1161
-        List<ChatMessage> messages = new ArrayList<>();
1162
-        messages.add(ChatMessage.ofUser(prompt));
1163
-        ChatSession chatSession = InMemoryChatSession.builder().messages(messages).build();
1164
-
1165
-        Prompt prompt1 = Prompt.of(prompt).attrPut("session", chatSession);
1166
-        ChatResponse response = chatModel.prompt(prompt1).call();
1167
-        String content = response.lastChoice().getMessage().getContent();
1168
-        return content;
1169
-    }
1170
-
1171
-    /**
1172
-     * 流式调用LLM生成回答 - 使用Flux
1173
-     * 
1174
-     * @param prompt 提示词
1175
-     * @return Flux<String> 流式输出(原始内容)
1176
-     */
1177
-    public Flux<String> generateAnswerFlux(String prompt) {
1178
-        ChatModel chatModel = ChatModel.of(llmServiceUrl)
1179
-                .model("Qwen")
1180
-                .timeout(Duration.ofSeconds(240))
1188
+        SimpleAgent robot = SimpleAgent.of(chatModel)
1181 1189
                 .build();
1182 1190
         List<ChatMessage> messages = new ArrayList<>();
1183 1191
         messages.add(ChatMessage.ofUser(prompt));
1184 1192
         ChatSession chatSession = InMemoryChatSession.builder().messages(messages).build();
1185 1193
 
1186 1194
         Prompt prompt1 = Prompt.of(prompt).attrPut("session", chatSession);
1187
-        return chatModel.prompt(prompt1).stream()
1188
-                .map(resp -> resp.getContent())
1189
-                .filter(content -> content != null && !content.isEmpty());
1195
+        String content = robot.prompt(prompt1).call().getContent();
1196
+        return content;
1190 1197
     }
1191 1198
 
1192 1199
     /**
@@ -1202,6 +1209,8 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
1202 1209
                 .model("Qwen")
1203 1210
                 .timeout(Duration.ofSeconds(240))
1204 1211
                 .build();
1212
+        SimpleAgent robot = SimpleAgent.of(chatModel)
1213
+                .build();
1205 1214
         List<ChatMessage> messages = new ArrayList<>();
1206 1215
         messages.add(ChatMessage.ofUser(prompt));
1207 1216
         ChatSession chatSession = InMemoryChatSession.builder().messages(messages).build();
@@ -1213,34 +1222,34 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
1213 1222
             tokenUsage[0].set((long) prompt.length());
1214 1223
         }
1215 1224
 
1216
-        return chatModel.prompt(prompt1).stream()
1225
+        return robot.prompt(prompt1).stream()
1217 1226
                 .doOnNext(resp -> {
1218 1227
                     if (tokenUsage != null && tokenUsage.length >= 3) {
1219 1228
                         try {
1220 1229
                             // 优先使用vLLM返回的usage信息(在最后一个chunk中)
1221
-                            AiUsage usage = resp.getUsage();
1222
-                            if (usage != null) {
1223
-                                Long pt = usage.promptTokens();
1224
-                                Long ct = usage.completionTokens();
1225
-                                Long tt = usage.totalTokens();
1226
-
1227
-                                if (pt > 0)
1228
-                                    tokenUsage[0].set(pt);
1229
-                                if (ct > 0)
1230
-                                    tokenUsage[1].set(ct);
1231
-                                if (tt > 0)
1232
-                                    tokenUsage[2].set(tt);
1233
-                            } else {
1234
-                                // 如果没有usage信息(中间chunk),估算completion tokens
1235
-                                String content = resp.getContent();
1236
-                                if (content != null && !content.isEmpty()) {
1237
-                                    // 中文大约1个字符=1个token,实时累计
1238
-                                    long currentCompletion = tokenUsage[1].get();
1239
-                                    long increment = content.length();
1240
-                                    tokenUsage[1].set(currentCompletion + increment);
1241
-                                    tokenUsage[2].set(tokenUsage[0].get() + tokenUsage[1].get());
1242
-                                }
1230
+                            // AiUsage usage = resp.getUsage();
1231
+                            // if (usage != null) {
1232
+                            // Long pt = usage.promptTokens();
1233
+                            // Long ct = usage.completionTokens();
1234
+                            // Long tt = usage.totalTokens();
1235
+
1236
+                            // if (pt > 0)
1237
+                            // tokenUsage[0].set(pt);
1238
+                            // if (ct > 0)
1239
+                            // tokenUsage[1].set(ct);
1240
+                            // if (tt > 0)
1241
+                            // tokenUsage[2].set(tt);
1242
+                            // } else {
1243
+                            // 如果没有usage信息(中间chunk),估算completion tokens
1244
+                            String content = resp.getContent();
1245
+                            if (content != null && !content.isEmpty()) {
1246
+                                // 中文大约1个字符=1个token,实时累计
1247
+                                long currentCompletion = tokenUsage[1].get();
1248
+                                long increment = content.length();
1249
+                                tokenUsage[1].set(currentCompletion + increment);
1250
+                                tokenUsage[2].set(tokenUsage[0].get() + tokenUsage[1].get());
1243 1251
                             }
1252
+                            // }
1244 1253
                         } catch (Exception e) {
1245 1254
                             // 如果获取usage失败,也尝试估算
1246 1255
                             try {
@@ -1333,7 +1342,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
1333 1342
         List<String> chapters = new ArrayList<>();
1334 1343
         String[] contentLines = content.split("\n");
1335 1344
         for (String line : contentLines) {
1336
-            if (line.contains("6."))
1345
+            if (line.startsWith("6.") || line.startsWith("1."))
1337 1346
                 chapters.add(line.replace("*", "").replace("#", "").replace("-", ""));
1338 1347
         }
1339 1348
         String prefixPath = "/upload/agent/" + agentName;
@@ -1628,7 +1637,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
1628 1637
 
1629 1638
             for (String line : lines) {
1630 1639
                 line = line.trim().replace("*", "").replace("#", "").replace("-", "");
1631
-                if (!line.contains("6.") || line.isEmpty()) {
1640
+                if (!line.startsWith("6.") && !line.startsWith("1.") || line.isEmpty()) {
1632 1641
                     continue;
1633 1642
                 }
1634 1643
 
@@ -1846,7 +1855,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
1846 1855
      * 分割文档
1847 1856
      */
1848 1857
     private List<TextSegment> splitDocument(File transferFile, int maxSegmentSizeInChars, int maxOverlapSizeInChars)
1849
-            throws IOException {
1858
+            throws Throwable {
1850 1859
         String filename = transferFile.getName().toLowerCase();
1851 1860
         try (InputStream fileInputStream = new FileInputStream(transferFile)) {
1852 1861
             if (filename.endsWith(".docx")) {
@@ -2026,7 +2035,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
2026 2035
                     // 当前行按普通段落处理
2027 2036
                     blocks.add(new Block("para", line));
2028 2037
                 } else if (inTable) {
2029
-                    tableLines.add(line);                
2038
+                    tableLines.add(line);
2030 2039
                 } else {
2031 2040
                     // 匹配格式:1. **文本**(数字前缀+加粗)
2032 2041
                     Pattern numberedBoldPattern = Pattern.compile("^(\\d+\\.\\s+)\\*\\*([^*]+)\\*\\*(.*)$");
@@ -2282,7 +2291,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
2282 2291
      */
2283 2292
     @Override
2284 2293
     public SseEmitter streamGenerateChapters(String topicId, MultipartFile file, String agentName, String selectedNode)
2285
-            throws IOException {
2294
+            throws Throwable {
2286 2295
         // 设置SSE超时时间为10分钟
2287 2296
         SseEmitter emitter = new SseEmitter(600000L);
2288 2297
 
@@ -2494,7 +2503,7 @@ public class CmcAgentServiceImpl implements ICmcAgentService {
2494 2503
 
2495 2504
                 emitter.complete();
2496 2505
 
2497
-            } catch (Exception e) {
2506
+            } catch (Throwable e) {
2498 2507
                 try {
2499 2508
                     emitter.send(SseEmitter.event()
2500 2509
                             .name("error")

+ 31
- 19
oa-ui/src/views/llm/agent/AgentDetail.vue Просмотреть файл

@@ -221,12 +221,11 @@
221 221
             <el-tab-pane label="文档上传" name="upload">
222 222
               <div class="task-content">
223 223
                 <div class="upload-section">
224
-                  <h4>上传招标文件</h4>
225 224
                   <el-upload class="upload-area" :action="uploadAction" :multiple="false" :auto-upload="false"
226 225
                     :file-list="taskFileList" :on-change="handleTaskFileChange" :before-upload="beforeUpload"
227 226
                     :show-file-list="true" :limit="1">
228 227
                     <el-button type="primary" size="large" icon="el-icon-upload">
229
-                      选择文件
228
+                      {{ selectDocument }}
230 229
                     </el-button>
231 230
                     <div class="upload-tip">支持 DOC、DOCX 格式</div>
232 231
                   </el-upload>
@@ -270,7 +269,7 @@
270 269
                       </div>
271 270
                     </div>
272 271
                   </el-tab-pane>
273
-                  <el-tab-pane :label="scoringTabLabel" name="scoring">
272
+                  <el-tab-pane v-if="!agentInfo.agentName.includes('设计')" :label="scoringTabLabel" name="scoring">
274 273
                     <div class="analysis-content">
275 274
                       <div v-if="analysisResults.scoring" class="analysis-result">
276 275
                         <div v-html="analysisResults.scoring"></div>
@@ -286,14 +285,14 @@
286 285
             </el-tab-pane>
287 286
 
288 287
             <!-- 标书内容 -->
289
-            <el-tab-pane label="标书内容" name="bid">
288
+            <el-tab-pane :label="bidTabLabel" name="bid">
290 289
               <div class="task-content">
291 290
                 <div class="bid-content">
292 291
                   <el-form :model="bidForm" label-width="70px">
293
-                    <el-form-item label="标书标题">
292
+                    <el-form-item :label="bidTitleLabel">
294 293
                       <div class="bid-title-row">
295 294
                         <div v-if="currentFilename" class="bid-title-display">
296
-                          <i class="el-icon-file-word"></i>
295
+                          <i clabidTabLabels="el-icon-file-word"></i>
297 296
                           <span>{{ currentFilename }}</span>
298 297
                         </div>
299 298
                         <el-upload v-else class="upload-area" :action="uploadAction" :multiple="false"
@@ -303,7 +302,7 @@
303 302
                         </el-upload>
304 303
                         <el-button type="primary" @click="isStreaming ? stopGeneration() : generateBid()"
305 304
                           :disabled="!currentFilename && !isStreaming">
306
-                          {{ isStreaming ? '停止生成' : '生成标书' }}
305
+                          {{ isStreaming ? '停止生成' : generateButtonText }}
307 306
                         </el-button>
308 307
                         <el-button type="primary" icon="el-icon-download" @click="exportWord"
309 308
                           :disabled="isStreaming">
@@ -380,7 +379,7 @@
380 379
                         <div v-if="generationCompleted" class="empty-bid-content generation-completed"
381 380
                           style="color: #67c23a;">
382 381
                           <i class="el-icon-success" style="font-size: 64px; margin-bottom: 20px; opacity: 0.8;"></i>
383
-                          <p style="font-size: 18px; font-weight: 500;">生成标书完成</p>
382
+                          <p style="font-size: 18px; font-weight: 500;">生成完成</p>
384 383
                         </div>
385 384
                         <!-- 初始空状态(未选中章节且未开始生成) -->
386 385
                         <div v-if="!isStreaming && !generationCompleted && !bidResult" class="empty-bid-content">
@@ -528,6 +527,15 @@ export default {
528 527
   computed: {
529 528
     uploadAction() {
530 529
       return '/llm/agent/upload'
530
+    },
531
+    bidTabLabel() {
532
+      return this.agentInfo?.agentName.includes('设计') ? '设计书内容' : '标书内容'
533
+    },
534
+    bidTitleLabel() {
535
+      return this.agentInfo?.agentName.includes('设计') ? '设计书标题' : '标书标题'
536
+    },
537
+    generateButtonText() {
538
+      return this.agentInfo?.agentName.includes('设计') ? '生成设计书' : '生成标书'
531 539
     }
532 540
   },
533 541
   watch: {
@@ -563,7 +571,7 @@ export default {
563 571
 
564 572
         // 设置评分标准/应价人须知标签
565 573
         if (this.agentInfo?.agentName) {
566
-          if (this.agentInfo.agentName.includes('技术') && this.agentInfo.agentName.includes('询价')) {
574
+          if (this.agentInfo.agentName.includes('技术标书') && this.agentInfo.agentName.includes('询价')) {
567 575
             this.scoringTabLabel = '应价人须知'
568 576
           } else {
569 577
             this.scoringTabLabel = '评分标准'
@@ -574,10 +582,14 @@ export default {
574 582
         if (this.agentInfo?.agentName) {
575 583
           const res = await opening(this.agentInfo.agentName)
576 584
           this.openingMessage = res.data.resultContent;
577
-          if (this.agentInfo.agentName.includes('技术')) {
585
+          if (this.agentInfo.agentName.includes('技术标书')) {
578 586
             this.selectDocument = '选择招标文件'
579 587
             this.selectDocumentTip = '请上传您需要分析的招标文件(单个文件):'
580 588
           }
589
+          else if (this.agentInfo.agentName.includes('技术设计书')) {
590
+            this.selectDocument = '选择项目任务书'
591
+            this.selectDocumentTip = '请上传您需要分析的项目任务书(单个文件):'
592
+          }
581 593
           else if (this.agentInfo.agentName.includes('商务')) {
582 594
           }
583 595
           else if (this.agentInfo.agentName.includes('文档检查')) {
@@ -1050,7 +1062,7 @@ export default {
1050 1062
               isHtml: true // 标记这是HTML内容
1051 1063
             }
1052 1064
             this.chatMessages.push(uploadMessage);
1053
-            if (this.agentInfo.agentName.includes('技术'))
1065
+            if (this.agentInfo.agentName.includes('技术标书'))
1054 1066
               this.techUploadDisplay = true;
1055 1067
             // this.$message.success('文件上传成功');
1056 1068
             const topicId = response.data.topicId;
@@ -1377,7 +1389,7 @@ export default {
1377 1389
        */
1378 1390
     async generateBid() {
1379 1391
       if (this.bidFileList.length === 0) {
1380
-        this.$message.warning('请先上传标书文件')
1392
+        this.$message.warning('请先上传文件')
1381 1393
         return
1382 1394
       }
1383 1395
 
@@ -1391,7 +1403,7 @@ export default {
1391 1403
             file = await this.downloadFileAsBlob(bidFileUrl, bidTitle)
1392 1404
             this.bidFileList[0].raw = file
1393 1405
           } catch (error) {
1394
-            this.$message.error('下载标书文件失败')
1406
+            this.$message.error('下载文件失败')
1395 1407
             return
1396 1408
           }
1397 1409
         }
@@ -1427,8 +1439,8 @@ export default {
1427 1439
         this.fetchStreamGeneration(formData, bidTitle)
1428 1440
 
1429 1441
       } catch (error) {
1430
-        console.error('生成标书失败:', error)
1431
-        this.$message.error('生成标书失败')
1442
+        console.error('生成失败:', error)
1443
+        this.$message.error('生成失败')
1432 1444
         this.isStreaming = false
1433 1445
       }
1434 1446
     },
@@ -1443,7 +1455,7 @@ export default {
1443 1455
 
1444 1456
       const onError = (error) => {
1445 1457
         console.error('流式请求失败:', error)
1446
-        this.$message.error('生成标书失败: ' + error.message)
1458
+        this.$message.error('生成失败: ' + error.message)
1447 1459
         this.isStreaming = false
1448 1460
         this.abortController = null
1449 1461
         this.$nextTick(() => {
@@ -1490,7 +1502,7 @@ export default {
1490 1502
           this.bidFilePath = process.env.VUE_APP_BASE_API + '/profile/upload/agent/' + this.agentInfo.agentName + '/' + this.currentFilename
1491 1503
         }
1492 1504
 
1493
-        this.$message.success('标书生成完成')
1505
+        this.$message.success('生成完成')
1494 1506
         return
1495 1507
       }
1496 1508
 
@@ -1673,7 +1685,7 @@ export default {
1673 1685
             this.selectedChapterContent = '暂无内容'
1674 1686
           }
1675 1687
         } else {
1676
-          this.selectedChapterContent = '请先上传标书文件'
1688
+          this.selectedChapterContent = '请先上传文件'
1677 1689
         }
1678 1690
       } catch (error) {
1679 1691
         console.error('获取章节内容失败:', error)
@@ -1814,7 +1826,7 @@ export default {
1814 1826
         this.bidResult = `<h3 style="color: #409EFF; margin: 10px 0;">【${node.title}】</h3>
1815 1827
                           <div class="empty-bid-content" style="padding: 40px; text-align: center; color: #999;">
1816 1828
                            <p>${isLeafNode ? '暂无内容' : '该节点为目录节点,请选择子章节'}</p>
1817
-                           <p style="font-size: 12px; margin-top: 8px;">${isLeafNode ? '点击"生成标书"按钮生成该章节内容' : '请展开并选择具体章节'}</p>
1829
+                           <p style="font-size: 12px; margin-top: 8px;">${isLeafNode ? '点击"生成"按钮生成该章节内容' : '请展开并选择具体章节'}</p>
1818 1830
                          </div>`
1819 1831
       }
1820 1832
     },

Загрузка…
Отмена
Сохранить