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) { } } }