package com.xly.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.langchain4j.agent.tool.ToolExecutionRequest;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.ChatMessageDeserializer;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.ToolExecutionResultMessage;
import dev.langchain4j.data.message.UserMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.UnaryOperator;
/**
* 事件账本的**唯一**投影/渲染器 —— 前端历史与 LLM 上下文同源于 {@link LedgerService} 的事件流,
* 文案在这一处生成,不再散落在各控制器里。
*
*
LLM 上下文四段式({@link #project}),预算按 token 记账(本地保守估算
* {@link TokenEstimator},绝对值由 {@code llm.context-length}(镜像 Ollama 的
* OLLAMA_CONTEXT_LENGTH,经 OpenAI 兼容通道无法逐请求传 num_ctx)导出):
*
* - ① 稳定 system prompt + ② 进行中流程卡 —— 永不让位;
* - ④ 往事摘要区:预算外旧轮 → 确定性一行摘要(零模型调用);
* - ⑤ 近期原文区:按**整轮**纳入,旧轮工具结果压 {@value #TOOL_DIGEST_LEN} 字;
* - 当前轮:原样保真、工具配对不可破坏;当前轮自身超限时**轮内让位**——最早的工具结果
* 降为摘要(保最近 {@value #CURRENT_KEEP_FULL} 个全文),单条结果超硬顶截断。
*
* 组装后做**发送前自检**:总 token 超预算 → ⑤ 最旧轮退入 ④、再裁 ④ 最旧行;①②与当前轮不动。
* 绝不把超限上下文交给 Ollama 静默截头(它从最前截、① 先死)。
*/
@Service
public class EventProjectionService {
private static final Logger log = LoggerFactory.getLogger(EventProjectionService.class);
public static final int TOOL_DIGEST_LEN = 120;
private static final int DIGEST_TURNS_MAX = 40;
private static final int DIGEST_LINE_LEN = 90;
/** 模型输出预留(num_ctx 覆盖 prompt+生成)。 */
private static final int OUTPUT_RESERVE = 1024;
/** 工具 schema(随每次请求发送、不在 messages 里)的估算开销。 */
private static final int TOOLS_OVERHEAD = 2000;
/** 当前轮增长的乐观小额预留(每次工具往返都会重投影,超出时反应式收缩⑤)。 */
private static final int TURN_RESERVE = 512;
/** ④ 摘要区 token 上限。 */
private static final int DIGEST_BUDGET = 800;
/** 当前轮里保全文的最近工具结果个数(轮内让位时更早的降摘要)。 */
private static final int CURRENT_KEEP_FULL = 2;
/** 当前轮单条工具结果硬顶(token),超出截断。 */
private static final int SINGLE_RESULT_CAP = 2400;
private final ObjectMapper mapper;
private final int contextLength;
/** opId → 人类可读状态行(Phase C:流程卡只读展示 ai_op_queue.sStatus)。可空(测试/未装配)。 */
private volatile UnaryOperator opStatusLookup;
public EventProjectionService(ObjectMapper mapper, @Value("${llm.context-length:16384}") int contextLength) {
this.mapper = mapper;
this.contextLength = contextLength;
}
public EventProjectionService(ObjectMapper mapper) {
this(mapper, 16384);
}
public void setOpStatusLookup(UnaryOperator lookup) {
this.opStatusLookup = lookup;
}
/** 输入 prompt 的总预算(token)。 */
int promptBudget() {
return Math.max(2048, contextLength - OUTPUT_RESERVE - TOOLS_OVERHEAD);
}
// ---------------------------------------------------------------- LLM 投影
/** 四段式 LLM 上下文(token 记账)。systemText 为空时不出 system 消息(如测试)。 */
public List project(String systemText, List