This commit is contained in:
Jordan Diaz
2026-04-09 20:46:03 +00:00
parent 4c73d848bb
commit 237dc00379
10 changed files with 1049 additions and 1216 deletions

View File

@@ -7,6 +7,7 @@ while preserving the most important information.
from __future__ import annotations
import hashlib
import json
import logging
import re
from typing import Any
@@ -157,6 +158,140 @@ class ContextCompactor:
break
return "\n".join(lines)
def compact_conversation(
self,
messages: list[dict[str, Any]],
max_tokens: int,
recent_raw_limit: int = 2,
raw_char_limit: int = 2000,
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
"""Compact conversation history while preserving the latest user turn."""
total = sum(self._estimate_message_tokens(m) for m in messages)
meta = {
"budget_tokens": max_tokens,
"input_tokens": total,
"output_tokens": total,
"messages_input": len(messages),
"messages_output": len(messages),
"messages_compacted": 0,
"tool_messages_compacted": 0,
"assistant_messages_compacted": 0,
"user_messages_compacted": 0,
"raw_tool_results_kept": 0,
}
if total <= max_tokens:
return messages, meta
compacted = [dict(m) for m in messages]
last_user_idx = max(
(i for i, m in enumerate(compacted) if m.get("role") == "user"),
default=-1,
)
tool_indexes = [i for i, m in enumerate(compacted) if m.get("role") == "tool"]
keep_raw_tool_indexes = (
set(tool_indexes[-recent_raw_limit:])
if recent_raw_limit > 0
else set()
)
for idx in keep_raw_tool_indexes:
content = compacted[idx].get("content", "")
if isinstance(content, str) and content:
truncated = content[:raw_char_limit]
if truncated != content:
compacted[idx]["content"] = truncated
meta["messages_compacted"] += 1
meta["tool_messages_compacted"] += 1
meta["raw_tool_results_kept"] += 1
total = sum(self._estimate_message_tokens(m) for m in compacted)
if total > max_tokens:
for idx in tool_indexes:
if idx in keep_raw_tool_indexes:
continue
content = compacted[idx].get("content", "")
if not isinstance(content, str) or not content:
continue
compacted[idx]["content"] = self._summarize_message_content(
content,
prefix="[TOOL RESULT COMPACTADO]",
max_chars=max(180, raw_char_limit // 4),
)
meta["messages_compacted"] += 1
meta["tool_messages_compacted"] += 1
total = sum(self._estimate_message_tokens(m) for m in compacted)
if total <= max_tokens:
break
if total > max_tokens:
for idx, message in enumerate(compacted):
if idx == last_user_idx or message.get("role") != "assistant":
continue
content = message.get("content", "")
if not isinstance(content, str) or not content:
continue
message["content"] = self._summarize_message_content(
content,
prefix="[ASSISTANT COMPACTADO]",
max_chars=max(240, raw_char_limit // 3),
)
meta["messages_compacted"] += 1
meta["assistant_messages_compacted"] += 1
total = sum(self._estimate_message_tokens(m) for m in compacted)
if total <= max_tokens:
break
if total > max_tokens:
for idx, message in enumerate(compacted):
if idx == last_user_idx or message.get("role") != "user":
continue
content = message.get("content", "")
if not isinstance(content, str) or not content:
continue
message["content"] = self._summarize_message_content(
content,
prefix="[USER CONTEXT COMPACTADO]",
max_chars=max(220, raw_char_limit // 3),
)
meta["messages_compacted"] += 1
meta["user_messages_compacted"] += 1
total = sum(self._estimate_message_tokens(m) for m in compacted)
if total <= max_tokens:
break
if total > max_tokens:
for idx in tool_indexes:
if idx in keep_raw_tool_indexes:
compacted[idx]["content"] = self._summarize_message_content(
compacted[idx].get("content", ""),
prefix="[TOOL RESULT COMPACTADO]",
max_chars=max(180, raw_char_limit // 5),
)
total = sum(self._estimate_message_tokens(m) for m in compacted)
if total <= max_tokens:
break
if total > max_tokens:
for idx, message in enumerate(compacted):
if idx == last_user_idx:
continue
role = message.get("role", "")
content = message.get("content", "")
if not isinstance(content, str) or not content:
continue
if role == "tool":
message["content"] = "[TOOL RESULT COMPACTADO]"
elif role == "assistant":
message["content"] = "[ASSISTANT COMPACTADO]"
elif role == "user":
message["content"] = "[USER CONTEXT COMPACTADO]"
total = sum(self._estimate_message_tokens(m) for m in compacted)
if total <= max_tokens:
break
meta["output_tokens"] = total
return compacted, meta
# ------------------------------------------------------------------
# Internals
# ------------------------------------------------------------------
@@ -186,6 +321,45 @@ class ContextCompactor:
compacted.append(line)
return "\n".join(compacted)
def _summarize_message_content(
self,
content: str,
prefix: str,
max_chars: int,
) -> str:
stripped = content.strip()
compacted = self._compact_text(content)
if len(compacted) <= max_chars:
if compacted != stripped:
summary = f"{prefix} {compacted}".strip()
if len(summary) > max_chars:
summary = summary[:max_chars].rstrip() + ""
return summary
return compacted
lines = [l.strip() for l in compacted.splitlines() if l.strip()]
if not lines:
return prefix
if len(lines) == 1:
return f"{prefix} {lines[0][:max_chars]}".strip()
first = lines[0][: max_chars // 2]
last = lines[-1][: max_chars // 3]
summary = f"{prefix} First: {first}"
if last and last != first:
summary += f" | Last: {last}"
if len(summary) > max_chars:
summary = summary[:max_chars].rstrip() + ""
return summary
@staticmethod
def _estimate_message_tokens(message: dict[str, Any]) -> int:
content = message.get("content", "")
tokens = estimate_tokens(content if isinstance(content, str) else str(content))
if message.get("tool_calls"):
tokens += estimate_tokens(json.dumps(message.get("tool_calls", []), ensure_ascii=False))
return tokens
def _extract_facts(self, raw_output: str) -> list[str]:
"""Extract short factual claims from tool output."""
facts: list[str] = []