|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+package com.huimv.employment.service.conversation;
|
|
|
2
|
+
|
|
|
3
|
+import com.huimv.employment.dao.entity.FeConversation;
|
|
|
4
|
+import com.huimv.employment.dao.mapper.FeConversationMapper;
|
|
|
5
|
+import com.huimv.employment.integration.kb.KbIntentClassificationClient;
|
|
|
6
|
+import org.slf4j.Logger;
|
|
|
7
|
+import org.slf4j.LoggerFactory;
|
|
|
8
|
+import org.springframework.scheduling.annotation.Async;
|
|
|
9
|
+import org.springframework.stereotype.Service;
|
|
|
10
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
11
|
+import org.springframework.util.StringUtils;
|
|
|
12
|
+
|
|
|
13
|
+import java.time.LocalDateTime;
|
|
|
14
|
+import java.util.Arrays;
|
|
|
15
|
+import java.util.Collections;
|
|
|
16
|
+import java.util.HashSet;
|
|
|
17
|
+import java.util.Set;
|
|
|
18
|
+
|
|
|
19
|
+/**
|
|
|
20
|
+ * 会话意图分类与标题更新(v2 §2.3)。
|
|
|
21
|
+ * <p>分类能力在智能体侧;本服务仅作为客户端调用智能体 {@code classify-intent},并落库更新会话标题。</p>
|
|
|
22
|
+ */
|
|
|
23
|
+@Service
|
|
|
24
|
+public class ConversationIntentService {
|
|
|
25
|
+
|
|
|
26
|
+ private static final Logger log = LoggerFactory.getLogger(ConversationIntentService.class);
|
|
|
27
|
+
|
|
|
28
|
+ private static final Set<String> STANDARD_INTENTS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
|
|
|
29
|
+ "employment", "knowledge", "order", "cost", "attendance", "emergency"
|
|
|
30
|
+ )));
|
|
|
31
|
+
|
|
|
32
|
+ private final ConversationService conversationService;
|
|
|
33
|
+ private final FeConversationMapper feConversationMapper;
|
|
|
34
|
+ private final KbIntentClassificationClient kbIntentClassificationClient;
|
|
|
35
|
+ private final ConversationIntentClassifier conversationIntentClassifier;
|
|
|
36
|
+
|
|
|
37
|
+ public ConversationIntentService(ConversationService conversationService,
|
|
|
38
|
+ FeConversationMapper feConversationMapper,
|
|
|
39
|
+ KbIntentClassificationClient kbIntentClassificationClient,
|
|
|
40
|
+ ConversationIntentClassifier conversationIntentClassifier) {
|
|
|
41
|
+ this.conversationService = conversationService;
|
|
|
42
|
+ this.feConversationMapper = feConversationMapper;
|
|
|
43
|
+ this.kbIntentClassificationClient = kbIntentClassificationClient;
|
|
|
44
|
+ this.conversationIntentClassifier = conversationIntentClassifier;
|
|
|
45
|
+ }
|
|
|
46
|
+
|
|
|
47
|
+ /**
|
|
|
48
|
+ * 首条消息后异步分类并更新标题,不阻塞主对话流。
|
|
|
49
|
+ */
|
|
|
50
|
+ @Async
|
|
|
51
|
+ public void classifyAndUpdateTitleAsync(String sessionId, String userMessage, String agentId) {
|
|
|
52
|
+ try {
|
|
|
53
|
+ classifyAndUpdateTitle(sessionId, userMessage, agentId);
|
|
|
54
|
+ } catch (Exception ex) {
|
|
|
55
|
+ log.warn("异步意图分类失败 sessionId={}: {}", sessionId, ex.getMessage());
|
|
|
56
|
+ }
|
|
|
57
|
+ }
|
|
|
58
|
+
|
|
|
59
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
60
|
+ void classifyAndUpdateTitle(String sessionId, String userMessage, String agentId) {
|
|
|
61
|
+ FeConversation conversation = conversationService.requireBySessionId(sessionId);
|
|
|
62
|
+ String summary = resolveSummary(userMessage, agentId);
|
|
|
63
|
+ applyClassification(conversation, summary, userMessage);
|
|
|
64
|
+ }
|
|
|
65
|
+
|
|
|
66
|
+ private String resolveSummary(String userMessage, String agentId) {
|
|
|
67
|
+ String upstream = kbIntentClassificationClient.classify(userMessage, agentId);
|
|
|
68
|
+ if (StringUtils.hasText(upstream)) {
|
|
|
69
|
+ return upstream.trim();
|
|
|
70
|
+ }
|
|
|
71
|
+ return conversationIntentClassifier.classify(userMessage);
|
|
|
72
|
+ }
|
|
|
73
|
+
|
|
|
74
|
+ private void applyClassification(FeConversation conversation, String summary, String userMessage) {
|
|
|
75
|
+ String chatType = resolveChatType(summary);
|
|
|
76
|
+ String title = resolveTitle(summary, userMessage);
|
|
|
77
|
+
|
|
|
78
|
+ FeConversation update = new FeConversation();
|
|
|
79
|
+ update.setId(conversation.getId());
|
|
|
80
|
+ update.setChatType(chatType);
|
|
|
81
|
+ update.setTitle(title);
|
|
|
82
|
+ update.setUpdateTime(LocalDateTime.now());
|
|
|
83
|
+ feConversationMapper.updateById(update);
|
|
|
84
|
+ log.debug("会话 {} 意图分类完成 summary={} title={}", conversation.getConversationNo(), summary, title);
|
|
|
85
|
+ }
|
|
|
86
|
+
|
|
|
87
|
+ private String resolveChatType(String summary) {
|
|
|
88
|
+ if (STANDARD_INTENTS.contains(summary)) {
|
|
|
89
|
+ return summary;
|
|
|
90
|
+ }
|
|
|
91
|
+ return "employment";
|
|
|
92
|
+ }
|
|
|
93
|
+
|
|
|
94
|
+ private String resolveTitle(String summary, String userMessage) {
|
|
|
95
|
+ if (!STANDARD_INTENTS.contains(summary)) {
|
|
|
96
|
+ return truncateTitle(summary);
|
|
|
97
|
+ }
|
|
|
98
|
+ switch (summary) {
|
|
|
99
|
+ case "employment":
|
|
|
100
|
+ return truncateTitle(StringUtils.hasText(userMessage) ? userMessage : "用工需求咨询");
|
|
|
101
|
+ case "knowledge":
|
|
|
102
|
+ return "知识咨询";
|
|
|
103
|
+ case "order":
|
|
|
104
|
+ return "订单管理";
|
|
|
105
|
+ case "cost":
|
|
|
106
|
+ return truncateTitle(StringUtils.hasText(userMessage) ? userMessage : "成本测算");
|
|
|
107
|
+ case "attendance":
|
|
|
108
|
+ return "现场考勤";
|
|
|
109
|
+ case "emergency":
|
|
|
110
|
+ return "紧急事件";
|
|
|
111
|
+ default:
|
|
|
112
|
+ return truncateTitle(userMessage);
|
|
|
113
|
+ }
|
|
|
114
|
+ }
|
|
|
115
|
+
|
|
|
116
|
+ private static String truncateTitle(String text) {
|
|
|
117
|
+ if (!StringUtils.hasText(text)) {
|
|
|
118
|
+ return null;
|
|
|
119
|
+ }
|
|
|
120
|
+ String trimmed = text.trim();
|
|
|
121
|
+ return trimmed.length() <= 200 ? trimmed : trimmed.substring(0, 200);
|
|
|
122
|
+ }
|
|
|
123
|
+}
|