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 用户消息尾部(稳住多轮上下文)。
*
*
键: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 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) {
}
}
}