Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 46 additions & 2 deletions astrbot/core/agent/context/compressor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import TYPE_CHECKING, Protocol, runtime_checkable

from ..message import Message
from ..message import Message, TextPart
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To support extracting tool call details from messages, we should import ToolCall from the message module.

Suggested change
from ..message import Message, TextPart
from ..message import Message, TextPart, ToolCall


if TYPE_CHECKING:
from astrbot import logger
Expand All @@ -17,6 +17,35 @@

from ..context.truncator import ContextTruncator

# Maximum number of characters to preserve from the tail of summarized messages.
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.

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)
Comment on lines +24 to +49
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When compressing messages, assistant messages often contain tool_calls instead of or in addition to standard text content (with content being None or empty). If we skip messages where content is None, we completely lose the context of what tools the assistant called, which makes the subsequent tool response messages in the history highly confusing to the LLM.

We should update extract_text_from_messages to also extract and format tool_calls when they are present.

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.

    Args:
        messages: The messages to extract text from.

    Returns:
        A concatenated string of all text content.
    """
    parts: list[str] = []
    for msg in messages:
        msg_text = ""
        if isinstance(msg.content, str):
            msg_text = msg.content
        elif isinstance(msg.content, list):
            text_segments = [
                part.text for part in msg.content if isinstance(part, TextPart)
            ]
            if text_segments:
                msg_text = "".join(text_segments)

        if msg.tool_calls:
            tool_calls_desc = []
            for tool_call in msg.tool_calls:
                if isinstance(tool_call, ToolCall):
                    name = tool_call.function.name
                    args = tool_call.function.arguments
                elif isinstance(tool_call, dict):
                    func = tool_call.get("function", {})
                    name = func.get("name", "")
                    args = func.get("arguments", "")
                else:
                    continue
                tool_calls_desc.append(f"call tool: {name}({args})")
            if tool_calls_desc:
                extra = "; ".join(tool_calls_desc)
                msg_text = f"{msg_text} [{extra}]" if msg_text else f"[{extra}]"

        if msg_text:
            parts.append(f"[{msg.role}]: {msg_text}")
    return "\n".join(parts)



@runtime_checkable
class ContextCompressor(Protocol):
Expand Down Expand Up @@ -227,14 +256,29 @@ 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 = extract_text_from_messages(messages_to_summarize)
if len(tail_text) > PRESERVE_TAIL_CHARS:
tail_text = tail_text[-PRESERVE_TAIL_CHARS:]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When the tail text exceeds PRESERVE_TAIL_CHARS and is truncated, prepending an ellipsis (...) helps indicate to the LLM that the text has been truncated from a longer conversation history.

Suggested change
tail_text = extract_text_from_messages(messages_to_summarize)
if len(tail_text) > PRESERVE_TAIL_CHARS:
tail_text = tail_text[-PRESERVE_TAIL_CHARS:]
tail_text = extract_text_from_messages(messages_to_summarize)
if len(tail_text) > PRESERVE_TAIL_CHARS:
tail_text = "..." + tail_text[-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(
Expand Down
Loading