"""会话摘要与滚动摘要(LLM + JSON)。""" from __future__ import annotations from typing import Any from app.core.langchain_llm import ainvoke_json_object, invoke_json_object from app.core.logging import get_logger from app.features.memory.llm_schemas import ( RollingSummaryPayload, SessionSummaryPayload, parse_json_payload, ) logger = get_logger(__name__) _ROLLING_SUMMARY_MERGE_RULES_ZH = ( "若新材料与已有摘要在同一人物或事件上存在明显事实冲突(如阵亡与在世、牺牲与退休、军衔或驻地去向矛盾)," "以新材料为准,删除或改写旧摘要中的矛盾句;不得把两处矛盾信息拼接成一句。" "不得将两则无因果关联的信息强行合成因果关系。" ) def _max_input_chars() -> int: from app.core.config import settings return settings.memory_enrichment_max_chars def generate_session_summary_sync(llm: Any, chunk_texts: list[str]) -> str: """为本批块生成 session 级短摘要。""" if not llm: return "" lim = _max_input_chars() combined = "\n\n".join(t for t in chunk_texts if t).strip()[:lim] if not combined: return "" prompt = ( "用 2~8 句中文概括下列口述/对话要点,不编造、不评价。只输出 JSON:" '{"summary":"..."}\n\n文本:\n' f"{combined}" ) try: raw = invoke_json_object( llm, prompt, max_tokens=2048, agent="memory.session_summary_sync" ) parsed = parse_json_payload(raw, SessionSummaryPayload) if parsed is None: return "" return str(parsed.summary or "").strip() except (TypeError, ValueError) as e: logger.warning("generate_session_summary_sync 失败: {}", e) return "" async def generate_session_summary_async(llm: Any, chunk_texts: list[str]) -> str: if not llm: return "" lim = _max_input_chars() combined = "\n\n".join(t for t in chunk_texts if t).strip()[:lim] if not combined: return "" prompt = ( "用 2~8 句中文概括下列口述/对话要点,不编造、不评价。只输出 JSON:" '{"summary":"..."}\n\n文本:\n' f"{combined}" ) try: raw = await ainvoke_json_object( llm, prompt, max_tokens=2048, agent="memory.session_summary_async" ) parsed = parse_json_payload(raw, SessionSummaryPayload) if parsed is None: return "" return str(parsed.summary or "").strip() except (TypeError, ValueError) as e: logger.warning("generate_session_summary_async 失败: {}", e) return "" def generate_rolling_summary_sync( llm: Any, existing_summary: str | None, new_chunk_texts: list[str] ) -> str: """合并已有滚动摘要与新材料。""" if not llm: return (existing_summary or "").strip() lim = _max_input_chars() new_t = "\n\n".join(t for t in new_chunk_texts if t).strip()[:lim] if not new_t and not (existing_summary or "").strip(): return "" ex = (existing_summary or "").strip()[:lim] prompt = ( "将「已有滚动摘要」与「新材料」合并为更新后的滚动摘要(中文,段落)。" "保留人物与时间线索;不编造;可省略无关细节。\n" f"{_ROLLING_SUMMARY_MERGE_RULES_ZH}\n" '只输出 JSON:{"rolling_summary":"..."}\n\n' f"【已有摘要】\n{ex}\n\n【新材料】\n{new_t}" ) try: raw = invoke_json_object( llm, prompt, max_tokens=3072, agent="memory.rolling_summary_sync" ) parsed = parse_json_payload(raw, RollingSummaryPayload) if parsed is None: return (existing_summary or "").strip() return str(parsed.rolling_summary or "").strip() except (TypeError, ValueError) as e: logger.warning("generate_rolling_summary_sync 失败: {}", e) return (existing_summary or "").strip() async def generate_rolling_summary_async( llm: Any, existing_summary: str | None, new_chunk_texts: list[str] ) -> str: if not llm: return (existing_summary or "").strip() lim = _max_input_chars() new_t = "\n\n".join(t for t in new_chunk_texts if t).strip()[:lim] if not new_t and not (existing_summary or "").strip(): return "" ex = (existing_summary or "").strip()[:lim] prompt = ( "将「已有滚动摘要」与「新材料」合并为更新后的滚动摘要(中文,段落)。" "保留人物与时间线索;不编造。\n" f"{_ROLLING_SUMMARY_MERGE_RULES_ZH}\n" '只输出 JSON:{"rolling_summary":"..."}\n\n' f"【已有摘要】\n{ex}\n\n【新材料】\n{new_t}" ) try: raw = await ainvoke_json_object( llm, prompt, max_tokens=3072, agent="memory.rolling_summary_async" ) parsed = parse_json_payload(raw, RollingSummaryPayload) if parsed is None: return (existing_summary or "").strip() return str(parsed.rolling_summary or "").strip() except (TypeError, ValueError) as e: logger.warning("generate_rolling_summary_async 失败: {}", e) return (existing_summary or "").strip()