110 lines
3.6 KiB
Python
110 lines
3.6 KiB
Python
"""LLM 上下文准备。
|
|
|
|
不改 Session 持久化历史,只在发给模型前做低风险压缩。第一阶段只压旧 tool
|
|
消息内容,保留 tool_call 协议字段,避免历史命令输出 / 检索结果反复占满 prompt。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from copy import deepcopy
|
|
from typing import Any, List
|
|
import json
|
|
import re
|
|
|
|
|
|
def _compact_old_tool_content(content: str, max_chars: int) -> str:
|
|
if len(content) <= max_chars:
|
|
return content
|
|
head = max_chars // 2
|
|
tail = max_chars - head
|
|
omitted = len(content) - head - tail
|
|
return (
|
|
content[:head]
|
|
+ f"\n[compacted old tool result, {omitted} chars omitted]\n"
|
|
+ content[-tail:]
|
|
)
|
|
|
|
|
|
_LOAD_SKILL_HEADER_RE = re.compile(r"\[skill=([^,\]]+)(?:,\s*dir=([^\]]+))?\]")
|
|
|
|
|
|
def _compact_load_skill_content(content: str) -> str:
|
|
first_line = content.splitlines()[0] if content else ""
|
|
match = _LOAD_SKILL_HEADER_RE.search(first_line)
|
|
if match:
|
|
skill = match.group(1)
|
|
skill_dir = match.group(2) or ""
|
|
suffix = f", dir={skill_dir}" if skill_dir else ""
|
|
return f"[loaded skill: {skill}{suffix}; full SKILL.md omitted from old context]"
|
|
return "[loaded skill; full SKILL.md omitted from old context]"
|
|
|
|
|
|
def _message_chars(msg: dict[str, Any]) -> int:
|
|
try:
|
|
return len(json.dumps(msg, ensure_ascii=False))
|
|
except TypeError:
|
|
return len(str(msg))
|
|
|
|
|
|
def prepare_messages_for_llm(
|
|
messages: List[dict[str, Any]],
|
|
*,
|
|
keep_recent: int = 20,
|
|
old_tool_chars: int = 2_000,
|
|
) -> List[dict[str, Any]]:
|
|
"""返回发给 LLM 的 messages 副本。
|
|
|
|
- system 和最近 keep_recent 条消息原样保留。
|
|
- 较旧且过长的 tool content 压缩为头尾摘要。
|
|
- role/tool_call_id/name 等协议字段不变。
|
|
"""
|
|
prepared, _ = prepare_messages_with_stats(
|
|
messages,
|
|
keep_recent=keep_recent,
|
|
old_tool_chars=old_tool_chars,
|
|
)
|
|
return prepared
|
|
|
|
|
|
def prepare_messages_with_stats(
|
|
messages: List[dict[str, Any]],
|
|
*,
|
|
keep_recent: int = 20,
|
|
old_tool_chars: int = 2_000,
|
|
) -> tuple[List[dict[str, Any]], dict[str, int]]:
|
|
"""返回发给 LLM 的 messages 副本和压缩统计。"""
|
|
if keep_recent < 0:
|
|
keep_recent = 0
|
|
original_chars = sum(_message_chars(m) for m in messages)
|
|
recent_start = max(0, len(messages) - keep_recent)
|
|
prepared: List[dict[str, Any]] = []
|
|
compacted_tool_messages = 0
|
|
compacted_skill_messages = 0
|
|
for idx, msg in enumerate(messages):
|
|
new_msg = deepcopy(msg)
|
|
is_recent = idx >= recent_start
|
|
if (
|
|
not is_recent
|
|
and new_msg.get("role") == "tool"
|
|
and isinstance(new_msg.get("content"), str)
|
|
):
|
|
before = new_msg["content"]
|
|
if new_msg.get("name") == "load_skill":
|
|
new_msg["content"] = _compact_load_skill_content(before)
|
|
compacted_skill_messages += int(new_msg["content"] != before)
|
|
else:
|
|
new_msg["content"] = _compact_old_tool_content(
|
|
before,
|
|
max_chars=max(0, old_tool_chars),
|
|
)
|
|
compacted_tool_messages += int(new_msg["content"] != before)
|
|
prepared.append(new_msg)
|
|
sent_chars = sum(_message_chars(m) for m in prepared)
|
|
stats = {
|
|
"original_chars": original_chars,
|
|
"sent_chars": sent_chars,
|
|
"saved_chars": max(0, original_chars - sent_chars),
|
|
"compacted_tool_messages": compacted_tool_messages,
|
|
"compacted_skill_messages": compacted_skill_messages,
|
|
}
|
|
return prepared, stats
|