ReMe/tests/light/test_tool_result_compactor.py
jinliyl dc8eab56a1
refactor(core): replace text truncation utilities with new marker system (#179)
* refactor(core): replace text truncation utilities with new marker system

- Remove old truncate_text_utils module and its exports
- Replace TRUNCATION_MARKER_START with _TRUNCATION_NOTICE_MARKER constant
- Update as_msg_stat.py to split content using new marker format
- Modify FileIO tool to use TRUNCATION_NOTICE_MARKER for continuation hints
- Change is_truncated function checks to use marker presence detection
- Move transformers dependency from main deps to light extra dependencies
- Update tool result compactor tests to verify marker instead of is_truncated calls

* feat(file_io): enhance file operations with path resolution and append functionality

- Add expanduser() to resolve file paths with ~ symbol
- Implement proper file existence and type validation in update_file
- Add new append_file method to append content to files
- Update truncation notice format for better readability
- Fix typo in error message from "provide" to "provided"
- Update transformers dependency in pyproject.toml
- Remove duplicate transformers dependency from light extras

* refactor(file_io): disable pylint too-many-return-statements warning

* perf(file_watcher): increase default polling delay and optimize watcher configuration

- Increased default poll_delay_ms from 1000ms to 2000ms to reduce CPU usage
- Removed force_polling parameter as it's no longer needed with updated polling strategy
- Simplified async watch configuration by removing conditional force_polling logic
- Reduced overall system resource consumption during file watching operations

* refactor(memory): update conversation log documentation in memory summary

- Changed "Raw conversation logs" to "Earlier conversation logs" for clarity
- Added warning note about potentially large dialog file sizes
- Improved formatting with additional line break for better readability
- Maintained existing compressed summary integration unchanged

* feat(memory): add long-term memory support to file-based memory system

- Initialize _long_term_memory attribute as empty string
- Add memories section to content when long-term memory exists
- Consolidate summary and memories into single user message
- Format memories with markdown header # Memories
- Maintain existing compressed summary functionality
- Join multiple content parts with double newlines
2026-03-26 12:10:24 +08:00

176 lines
7 KiB
Python

"""Tests for ToolResultCompactor."""
import asyncio
import tempfile
from datetime import datetime, timedelta
from pathlib import Path
from agentscope.message import Msg
from reme.memory.file_based.components import ToolResultCompactor
from reme.memory.file_based.utils import TRUNCATION_NOTICE_MARKER
def create_tool_result_msg(output: str | list, tool_name: str = "test_tool") -> Msg:
"""Create a Msg with tool_result content block."""
return Msg(
name="tool",
role="user",
content=[
{
"type": "tool_result",
"id": "call_123",
"name": tool_name,
"output": output,
},
],
)
class TestToolResultCompactor:
"""Tests for ToolResultCompactor."""
def test_no_truncation_when_under_threshold(self):
"""Test that short content is not truncated."""
with tempfile.TemporaryDirectory() as tmpdir:
op = ToolResultCompactor(tool_result_dir=tmpdir, recent_max_bytes=1000)
messages = [create_tool_result_msg("short content")]
result = asyncio.run(op.call(messages=messages))
assert result == messages
assert messages[0].content[0]["output"] == "short content"
assert len(list(Path(tmpdir).glob("*.txt"))) == 0
def test_truncation_when_over_threshold(self):
"""Test that long content is truncated and saved to file."""
with tempfile.TemporaryDirectory() as tmpdir:
op = ToolResultCompactor(tool_result_dir=tmpdir, recent_max_bytes=100)
long_content = "x" * 500
messages = [create_tool_result_msg(long_content)]
_ = asyncio.run(op.call(messages=messages))
output = messages[0].content[0]["output"]
assert TRUNCATION_NOTICE_MARKER in output
assert "[Full content saved to:" in output
# Verify file was created
files = list(Path(tmpdir).glob("*.txt"))
assert len(files) == 1
# Verify file content
content = files[0].read_text()
assert "# tool_name: test_tool" in content
assert "# created_at:" in content
assert long_content in content
def test_skip_already_truncated(self):
"""Test that already truncated content is not re-truncated."""
with tempfile.TemporaryDirectory() as tmpdir:
op = ToolResultCompactor(tool_result_dir=tmpdir, recent_max_bytes=100)
truncated_content = "head<<<TRUNCATED>>>(100 chars omitted)<<<END_TRUNCATED>>>tail"
messages = [create_tool_result_msg(truncated_content)]
asyncio.run(op.call(messages=messages))
assert messages[0].content[0]["output"] == truncated_content
assert len(list(Path(tmpdir).glob("*.txt"))) == 0
def test_truncation_list_output(self):
"""Test truncation of list output with text blocks."""
with tempfile.TemporaryDirectory() as tmpdir:
op = ToolResultCompactor(tool_result_dir=tmpdir, recent_max_bytes=100)
list_output = [{"type": "text", "text": "y" * 500}]
messages = [create_tool_result_msg(list_output)]
asyncio.run(op.call(messages=messages))
text_block = messages[0].content[0]["output"][0]
assert TRUNCATION_NOTICE_MARKER in text_block["text"]
assert len(list(Path(tmpdir).glob("*.txt"))) == 1
def test_list_output_no_truncation_when_short(self):
"""Test that short list output is not truncated."""
with tempfile.TemporaryDirectory() as tmpdir:
op = ToolResultCompactor(tool_result_dir=tmpdir, recent_max_bytes=1000)
list_output = [{"type": "text", "text": "short"}]
messages = [create_tool_result_msg(list_output)]
asyncio.run(op.call(messages=messages))
assert messages[0].content[0]["output"][0]["text"] == "short"
assert len(list(Path(tmpdir).glob("*.txt"))) == 0
def test_list_output_multiple_text_blocks(self):
"""Test truncation of multiple text blocks in list output."""
with tempfile.TemporaryDirectory() as tmpdir:
op = ToolResultCompactor(tool_result_dir=tmpdir, recent_max_bytes=100)
list_output = [
{"type": "text", "text": "a" * 500},
{"type": "text", "text": "short"},
{"type": "text", "text": "b" * 500},
]
messages = [create_tool_result_msg(list_output)]
asyncio.run(op.call(messages=messages))
output = messages[0].content[0]["output"]
assert TRUNCATION_NOTICE_MARKER in output[0]["text"]
assert output[1]["text"] == "short" # unchanged
assert TRUNCATION_NOTICE_MARKER in output[2]["text"]
assert len(list(Path(tmpdir).glob("*.txt"))) == 2
def test_list_output_mixed_block_types(self):
"""Test that non-text blocks in list output are unchanged."""
with tempfile.TemporaryDirectory() as tmpdir:
op = ToolResultCompactor(tool_result_dir=tmpdir, recent_max_bytes=100)
list_output = [
{"type": "text", "text": "c" * 500},
{"type": "image", "source": {"type": "url", "url": "http://example.com/img.png"}},
]
messages = [create_tool_result_msg(list_output)]
asyncio.run(op.call(messages=messages))
output = messages[0].content[0]["output"]
assert TRUNCATION_NOTICE_MARKER in output[0]["text"]
assert output[1] == {"type": "image", "source": {"type": "url", "url": "http://example.com/img.png"}}
assert len(list(Path(tmpdir).glob("*.txt"))) == 1
def test_cleanup_expired_files(self):
"""Test cleanup of expired files."""
with tempfile.TemporaryDirectory() as tmpdir:
op = ToolResultCompactor(tool_result_dir=tmpdir, recent_max_bytes=100, retention_days=1)
# Create an old file
old_time = (datetime.now() - timedelta(days=2)).isoformat()
old_file = Path(tmpdir) / "old_file.txt"
old_file.write_text(f"# tool_name: test\n# created_at: {old_time}\n# ---\ncontent")
# Create a new file
new_time = datetime.now().isoformat()
new_file = Path(tmpdir) / "new_file.txt"
new_file.write_text(f"# tool_name: test\n# created_at: {new_time}\n# ---\ncontent")
deleted = op.cleanup_expired_files()
assert deleted == 1
assert not old_file.exists()
assert new_file.exists()
def test_string_content_msg_unchanged(self):
"""Test that messages with string content are unchanged."""
with tempfile.TemporaryDirectory() as tmpdir:
op = ToolResultCompactor(tool_result_dir=tmpdir, recent_max_bytes=100)
messages = [Msg(name="user", role="user", content="hello world")]
asyncio.run(op.call(messages=messages))
assert messages[0].content == "hello world"
if __name__ == "__main__":
import pytest
pytest.main([__file__, "-v"])