StateService.java 7.88 KB
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) {
        }
    }
}