StateService.java
7.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
package com.xly.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.xly.agent.Intent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.List;
/**
* 会话状态槽 —— 由**代码**(而非模型总结)维护的少量结构化状态:上轮意图 / 最近实体 / 在办单据。
* 注入两处:意图门的输入前缀(让「那张单子」这类指代可解),agent 用户消息尾部(稳住多轮上下文)。
*
* <p>键:Redis HASH {@code chat:state:{convId}}(fields: intent/danju/entities/doc),30 天 TTL。
*/
@Service
public class StateService {
private static final Logger log = LoggerFactory.getLogger(StateService.class);
private static final String PREFIX = "chat:state:";
private static final Duration TTL = Duration.ofDays(30);
private static final int MAX_ENTITIES = 8;
private final StringRedisTemplate redis;
private final ObjectMapper mapper;
public StateService(StringRedisTemplate redis, ObjectMapper mapper) {
this.redis = redis;
this.mapper = mapper;
}
/** 记录本轮意图门结果(上轮意图 + 单据类型)。 */
public void recordIntent(String convId, String intent, String danju) {
try {
String key = PREFIX + convId;
redis.opsForHash().put(key, "intent", intent == null ? "" : intent);
redis.opsForHash().put(key, "danju", danju == null ? "" : danju);
redis.expire(key, TTL);
} catch (Exception e) {
log.warn("state recordIntent failed (conv={}): {}", convId, e.getMessage());
}
}
/** 合并本轮识别到的实体(最新在前、按 值+角色 去重、封顶 {@value #MAX_ENTITIES} 个)。 */
public void mergeEntities(String convId, List<Intent.Entity> entities) {
if (entities == null || entities.isEmpty()) {
return;
}
try {
String key = PREFIX + convId;
ArrayNode merged = mapper.createArrayNode();
for (Intent.Entity e : entities) {
if (e == null || e.value == null || e.value.isBlank()) continue;
ObjectNode n = merged.addObject();
n.put("value", e.value.trim());
n.put("role", e.role == null ? "未知" : e.role);
}
Object old = redis.opsForHash().get(key, "entities");
if (old != null) {
JsonNode arr = mapper.readTree(old.toString());
for (JsonNode n : arr) {
if (merged.size() >= MAX_ENTITIES) break;
boolean dup = false;
for (JsonNode m : merged) {
if (m.path("value").asText().equals(n.path("value").asText())
&& m.path("role").asText().equals(n.path("role").asText())) {
dup = true;
break;
}
}
if (!dup) merged.add(n);
}
}
redis.opsForHash().put(key, "entities", mapper.writeValueAsString(merged));
redis.expire(key, TTL);
} catch (Exception e) {
log.warn("state mergeEntities failed (conv={}): {}", convId, e.getMessage());
}
}
/** 设置在办单据(entity=单据/实体类型,record=记录名/单号,stage=collecting|proposed|executed|failed|cancelled)。 */
public void setActiveDoc(String convId, String entity, String record, String opId, String stage) {
try {
String key = PREFIX + convId;
ObjectNode doc = mapper.createObjectNode();
doc.put("entity", entity == null ? "" : entity);
doc.put("record", record == null ? "" : record);
doc.put("opId", opId == null ? "" : opId);
doc.put("stage", stage == null ? "" : stage);
redis.opsForHash().put(key, "doc", mapper.writeValueAsString(doc));
redis.expire(key, TTL);
} catch (Exception e) {
log.warn("state setActiveDoc failed (conv={}): {}", convId, e.getMessage());
}
}
/** 确认/取消后推进在办单据阶段(仅当 opId 匹配当前在办单据)。 */
public void updateDocStage(String convId, String opId, String stage) {
if (convId == null || convId.isBlank() || opId == null || opId.isBlank()) {
return;
}
try {
String key = PREFIX + convId;
Object old = redis.opsForHash().get(key, "doc");
if (old == null) {
return;
}
ObjectNode doc = (ObjectNode) mapper.readTree(old.toString());
if (!opId.equals(doc.path("opId").asText(""))) {
return;
}
doc.put("stage", stage);
redis.opsForHash().put(key, "doc", mapper.writeValueAsString(doc));
} catch (Exception e) {
log.warn("state updateDocStage failed (conv={}): {}", convId, e.getMessage());
}
}
/** 状态摘要(一行中文),空状态返回 ""。喂意图门 + 附在 agent 用户消息尾部。 */
public String digest(String convId) {
try {
String key = PREFIX + convId;
Object intent = redis.opsForHash().get(key, "intent");
Object danju = redis.opsForHash().get(key, "danju");
Object doc = redis.opsForHash().get(key, "doc");
Object entities = redis.opsForHash().get(key, "entities");
StringBuilder sb = new StringBuilder();
if (intent != null && !intent.toString().isBlank()) {
sb.append("上轮意图=").append(intent);
if (danju != null && !danju.toString().isBlank()) {
sb.append("(").append(danju).append(")");
}
}
if (doc != null) {
JsonNode d = mapper.readTree(doc.toString());
String ent = d.path("entity").asText("");
String rec = d.path("record").asText("");
String stage = d.path("stage").asText("");
if (!ent.isBlank() || !rec.isBlank()) {
if (sb.length() > 0) sb.append(";");
sb.append("在办单据=").append(ent);
if (!rec.isBlank()) sb.append("【").append(rec).append("】");
if (!stage.isBlank()) sb.append("(").append(stageZh(stage)).append(")");
}
}
if (entities != null) {
JsonNode arr = mapper.readTree(entities.toString());
StringBuilder es = new StringBuilder();
for (JsonNode n : arr) {
if (es.length() > 0) es.append("、");
es.append(n.path("role").asText("未知")).append("=").append(n.path("value").asText(""));
}
if (es.length() > 0) {
if (sb.length() > 0) sb.append(";");
sb.append("最近实体=").append(es);
}
}
return sb.toString();
} catch (Exception e) {
return "";
}
}
private static String stageZh(String stage) {
switch (stage) {
case "collecting": return "填表中";
case "proposed": return "待确认";
case "executed": return "已执行";
case "failed": return "执行失败";
case "cancelled": return "已取消";
default: return stage;
}
}
public void delete(String convId) {
try {
redis.delete(PREFIX + convId);
} catch (Exception ignore) {
}
}
}