diff --git a/astrbot/core/agent/context/compressor.py b/astrbot/core/agent/context/compressor.py index d4642bc506..1d1cf411d1 100644 --- a/astrbot/core/agent/context/compressor.py +++ b/astrbot/core/agent/context/compressor.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, Protocol, runtime_checkable -from ..message import Message +from ..message import Message, TextPart if TYPE_CHECKING: from astrbot import logger @@ -17,6 +17,37 @@ from ..context.truncator import ContextTruncator +# Default number of characters to preserve from the tail of summarized messages. +DEFAULT_PRESERVE_TAIL_CHARS = 10000 + + +def extract_text_from_messages(messages: list[Message]) -> str: + """Extract text content from a list of messages into a single string. + + Each message is formatted as "[role]: content" for readability. + Only text content is extracted; tool_calls are intentionally omitted + as they are verbose and already covered by the LLM-generated summary. + + Args: + messages: The messages to extract text from. + + Returns: + A concatenated string of all text content. + """ + parts: list[str] = [] + for msg in messages: + if msg.content is None: + continue + if isinstance(msg.content, str): + parts.append(f"[{msg.role}]: {msg.content}") + elif isinstance(msg.content, list): + text_segments = [ + part.text for part in msg.content if isinstance(part, TextPart) + ] + if text_segments: + parts.append(f"[{msg.role}]: {''.join(text_segments)}") + return "\n".join(parts) + @runtime_checkable class ContextCompressor(Protocol): @@ -154,6 +185,7 @@ def __init__( keep_recent: int = 4, instruction_text: str | None = None, compression_threshold: float = 0.82, + preserve_tail_chars: int = DEFAULT_PRESERVE_TAIL_CHARS, ) -> None: """Initialize the LLM summary compressor. @@ -162,10 +194,13 @@ def __init__( keep_recent: The number of latest messages to keep (default: 4). instruction_text: Custom instruction for summary generation. compression_threshold: The compression trigger threshold (default: 0.82). + preserve_tail_chars: Maximum characters to preserve from the tail of + summarized messages (default: 10000). Set to 0 to disable. """ self.provider = provider self.keep_recent = keep_recent self.compression_threshold = compression_threshold + self.preserve_tail_chars = preserve_tail_chars self.instruction_text = instruction_text or ( "Based on our full conversation history, produce a concise summary of key takeaways and/or project progress.\n" @@ -227,14 +262,31 @@ async def __call__(self, messages: list[Message]) -> list[Message]: logger.warning("LLM context compression returned an empty summary.") return messages + # Extract the tail of the original conversation text to preserve recent details. + # This ensures the compressed context retains both a high-level summary + # and the most recent raw conversation from the summarized portion. + tail_text = "" + if self.preserve_tail_chars > 0: + tail_text = extract_text_from_messages(messages_to_summarize) + if len(tail_text) > self.preserve_tail_chars: + tail_text = "..." + tail_text[-self.preserve_tail_chars :] + # build result result = [] result.extend(system_messages) + compressed_content = ( + f"Our previous history conversation summary:\n{summary_content}" + ) + if tail_text: + compressed_content += ( + f"\n\n---\nRecent conversation details before compression:\n{tail_text}" + ) + result.append( Message( role="user", - content=f"Our previous history conversation summary: {summary_content}", + content=compressed_content, ) ) result.append(