mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-09-05 08:06:15 +00:00
* update * refactor(memory): remove unnecessary type check and update error logging * refactor(core): standardize logger import and update agentscope dependency * fix(memory): disable console output and add logging for summarizer component * feat(core): replace OpenAI token counter with custom ReMe token counter - Replace OpenAITokenCounter with ReMeTokenCounter implementation - Add support for HuggingFace mirror and configurable tokenizer - Register ReMeTokenCounter as default token counter in registry - Update config to use hf backend with Qwen2.5-7B-Instruct model refactor(memory): convert token counting methods to async in message handlers - Change count_str_token, stat_message, count_msgs_token to async methods - Update format_msgs_to_str and context_check to use async token counting - Modify _format_tool_result_output to support async token counting - Adjust all dependent methods to await async token counting calls feat(memory): add dialog persistence to in-memory storage - Implement _append_messages_to_dialog for saving messages to JSONL files - Add dialog_path parameter to ReMeInMemoryMemory constructor - Persist messages to daily JSONL files based on timestamp grouping - Update mark_messages_compressed to save and remove compressed messages - Modify clear_content to persist all messages before clearing memory refactor(ops): update token counter type hints and initialization - Change BaseOp to use HuggingFaceTokenCounter instead of TokenCounterBase - Update type annotations for as_token_counter property and parameters - Remove direct token counter injection from Compactor and ContextChecker - Pass as_token_counter parameter through service context mechanism style(logging): improve error logging with exception details - Replace logger.error with logger.exception in browser control tool - Change logger.error to logger.exception in memory get tool error handling - Add proper exception logging with stack trace information chore(config): add token counter configuration to light YAML - Add as_token_counters section with default hf backend configuration - Configure Qwen/Qwen2.5-7B-Instruct model with mirror support enabled - Set up pretrained_model_name_or_path and use_mirror parameters test(context): update context check tests to async implementation - Convert verify_context_check_invariants to async function - Update context check test methods to use async calls - Change stat_message calls to await async implementation - Modify test_empty_messages and test_below_threshold_returns_all to async * feat(core): implement context checking and memory management features * refactor(core): replace direct loguru import with logger utility function * refactor(reme): remove RuntimeContext dependency and simplify context checking * feat(docs): add raw conversation persistence to ReMe framework
371 lines
12 KiB
Python
371 lines
12 KiB
Python
"""Tests for Compactor."""
|
|
|
|
import asyncio
|
|
|
|
from agentscope.message import Msg
|
|
from test_utils import (
|
|
get_dash_chat_model,
|
|
get_formatter,
|
|
get_token_counter,
|
|
)
|
|
|
|
from reme.core.utils import get_logger
|
|
from reme.memory.file_based.components import Compactor
|
|
|
|
logger = get_logger()
|
|
|
|
|
|
# ANSI 颜色码
|
|
class Colors:
|
|
"""ANSI color codes for terminal output."""
|
|
|
|
GREEN = "\033[92m"
|
|
RED = "\033[91m"
|
|
YELLOW = "\033[93m"
|
|
BLUE = "\033[94m"
|
|
CYAN = "\033[96m"
|
|
BOLD = "\033[1m"
|
|
RESET = "\033[0m"
|
|
|
|
|
|
def print_pass(test_name: str):
|
|
"""打印测试通过信息"""
|
|
print(f"{Colors.GREEN}{Colors.BOLD}✓ {test_name} PASSED{Colors.RESET}")
|
|
|
|
|
|
def print_fail(test_name: str, error: str):
|
|
"""打印测试失败信息"""
|
|
print(f"{Colors.RED}{Colors.BOLD}✗ {test_name} FAILED: {error}{Colors.RESET}")
|
|
|
|
|
|
def print_error(test_name: str, error: str):
|
|
"""打印测试错误信息"""
|
|
print(f"{Colors.YELLOW}{Colors.BOLD}⚠ {test_name} ERROR: {error}{Colors.RESET}")
|
|
|
|
|
|
def print_test_header(test_name: str):
|
|
"""打印测试标题"""
|
|
print(f"\n{Colors.CYAN}{'=' * 60}{Colors.RESET}")
|
|
print(f"{Colors.BLUE}{Colors.BOLD}Running: {test_name}{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'=' * 60}{Colors.RESET}")
|
|
|
|
|
|
def create_user_msg(content: str) -> Msg:
|
|
"""Create a user message."""
|
|
return Msg(name="user", role="user", content=content)
|
|
|
|
|
|
def create_assistant_msg(content: str) -> Msg:
|
|
"""Create an assistant message."""
|
|
return Msg(name="assistant", role="assistant", content=content)
|
|
|
|
|
|
def create_tool_use_msg(tool_name: str, tool_input: dict) -> Msg:
|
|
"""Create a message with tool_use content block."""
|
|
return Msg(
|
|
name="assistant",
|
|
role="assistant",
|
|
content=[
|
|
{
|
|
"type": "tool_use",
|
|
"id": "call_123",
|
|
"name": tool_name,
|
|
"input": tool_input,
|
|
},
|
|
],
|
|
)
|
|
|
|
|
|
def create_tool_result_msg(tool_name: str, output: str) -> Msg:
|
|
"""Create a message with tool_result content block."""
|
|
return Msg(
|
|
name="tool",
|
|
role="user",
|
|
content=[
|
|
{
|
|
"type": "tool_result",
|
|
"id": "call_123",
|
|
"name": tool_name,
|
|
"output": output,
|
|
},
|
|
],
|
|
)
|
|
|
|
|
|
def create_compactor():
|
|
"""Create a Compactor instance for testing."""
|
|
return Compactor(
|
|
memory_compact_threshold=4000,
|
|
as_token_counter=get_token_counter(),
|
|
as_llm=get_dash_chat_model(),
|
|
as_llm_formatter=get_formatter(),
|
|
language="zh",
|
|
)
|
|
|
|
|
|
def test_empty_messages():
|
|
"""Test that empty messages return empty string."""
|
|
compactor = create_compactor()
|
|
result = asyncio.run(compactor.call(messages=[]))
|
|
assert result == "", f"Expected empty string, got: {result}"
|
|
print("test_empty_messages PASSED")
|
|
|
|
|
|
def test_short_conversation():
|
|
"""Test compaction of a short conversation."""
|
|
compactor = create_compactor()
|
|
messages = [
|
|
create_user_msg("Hello, I need help with Python."),
|
|
create_assistant_msg("Sure, I'd be happy to help. What do you need?"),
|
|
create_user_msg("How do I read a file?"),
|
|
create_assistant_msg("You can use open() function: with open('file.txt', 'r') as f: content = f.read()"),
|
|
]
|
|
|
|
logger.info(f"Input messages count: {len(messages)}")
|
|
for i, msg in enumerate(messages):
|
|
logger.debug(
|
|
f"Message {i}: role={msg.role}, content="
|
|
f"{msg.content[:50] if isinstance(msg.content, str) else msg.content}...",
|
|
)
|
|
|
|
result = asyncio.run(compactor.call(messages=messages))
|
|
|
|
logger.info(f"Result type: {type(result)}")
|
|
logger.info(f"Result: {result}")
|
|
|
|
assert result, "Result should not be empty"
|
|
assert isinstance(result, str), f"Result should be string, got: {type(result)}"
|
|
assert "##" in result, "Result should have markdown headers"
|
|
print_pass("test_short_conversation")
|
|
|
|
|
|
def test_medium_conversation():
|
|
"""Test compaction of a medium-length conversation with tool calls."""
|
|
compactor = create_compactor()
|
|
messages = [
|
|
create_user_msg("Help me create a Python script to process data."),
|
|
create_assistant_msg("I'll help you create a data processing script. Let me first check the data format."),
|
|
create_tool_use_msg("read_file", {"path": "/data/input.csv"}),
|
|
create_tool_result_msg("read_file", "id,name,value\n1,Alice,100\n2,Bob,200\n3,Charlie,300"),
|
|
create_assistant_msg(
|
|
"I see the data is in CSV format. Here's a script to process it:\n"
|
|
"```python\nimport csv\n\ndef process_data(filepath):\n"
|
|
" with open(filepath, 'r') as f:\n reader = csv.DictReader(f)\n"
|
|
" return [row for row in reader]\n```",
|
|
),
|
|
create_user_msg("Can you add a filter function?"),
|
|
create_assistant_msg(
|
|
"Sure, here's the updated script with filtering:\n"
|
|
"```python\ndef filter_by_value(data, min_value):\n"
|
|
" return [row for row in data if int(row['value']) >= min_value]\n```",
|
|
),
|
|
]
|
|
|
|
result = asyncio.run(compactor.call(messages=messages))
|
|
|
|
assert result, "Result should not be empty"
|
|
assert isinstance(result, str), f"Result should be string, got: {type(result)}"
|
|
assert "##" in result, "Result should have markdown headers"
|
|
print_pass("test_medium_conversation")
|
|
|
|
|
|
def test_long_conversation():
|
|
"""Test compaction of a long conversation that exceeds threshold."""
|
|
compactor = create_compactor()
|
|
messages = []
|
|
for i in range(100000):
|
|
messages.append(create_user_msg(f"Question {i}: How do I implement feature {i}?"))
|
|
messages.append(
|
|
create_assistant_msg(
|
|
f"Answer {i}: Here's how to implement feature {i}. " * 20,
|
|
),
|
|
)
|
|
|
|
result = asyncio.run(compactor.call(messages=messages))
|
|
|
|
assert result, "Result should not be empty"
|
|
assert isinstance(result, str), f"Result should be string, got: {type(result)}"
|
|
assert "##" in result, "Result should have markdown headers"
|
|
print_pass("test_long_conversation")
|
|
|
|
|
|
def test_with_previous_summary():
|
|
"""Test compaction with an existing previous summary."""
|
|
compactor = create_compactor()
|
|
previous_summary = """## Goal
|
|
User wants to build a REST API with FastAPI.
|
|
|
|
## Constraints & Preferences
|
|
- Use Python 3.10+
|
|
- Follow RESTful best practices
|
|
|
|
## Progress
|
|
### Done
|
|
- [x] Set up project structure
|
|
- [x] Created main.py with basic FastAPI app
|
|
|
|
### In Progress
|
|
- [ ] Add user authentication
|
|
|
|
### Blocked
|
|
- (none)
|
|
|
|
## Key Decisions
|
|
- **Framework**: FastAPI for performance and type hints
|
|
|
|
## Next Steps
|
|
1. Implement JWT authentication
|
|
2. Add user endpoints
|
|
|
|
## Critical Context
|
|
- Using SQLAlchemy for database
|
|
- PostgreSQL as database backend
|
|
"""
|
|
|
|
messages = [
|
|
create_user_msg("Let's implement the JWT authentication now."),
|
|
create_assistant_msg("I'll implement JWT authentication. First, let me install the required packages."),
|
|
create_tool_use_msg("run_command", {"command": "pip install python-jose[cryptography] passlib[bcrypt]"}),
|
|
create_tool_result_msg("run_command", "Successfully installed python-jose-3.3.0 passlib-1.7.4"),
|
|
create_assistant_msg("Dependencies installed. Now let's create the auth module with JWT token generation."),
|
|
]
|
|
|
|
result = asyncio.run(
|
|
compactor.call(
|
|
messages=messages,
|
|
previous_summary=previous_summary,
|
|
),
|
|
)
|
|
|
|
assert result, "Result should not be empty"
|
|
assert isinstance(result, str), f"Result should be string, got: {type(result)}"
|
|
assert "##" in result, "Result should have markdown headers"
|
|
print_pass("test_with_previous_summary")
|
|
|
|
|
|
def test_conversation_with_multiple_tool_calls():
|
|
"""Test compaction of conversation with multiple sequential tool calls."""
|
|
compactor = create_compactor()
|
|
messages = [
|
|
create_user_msg("Help me debug this Python script that's failing."),
|
|
create_assistant_msg("Let me check the script first."),
|
|
create_tool_use_msg("read_file", {"path": "/app/main.py"}),
|
|
create_tool_result_msg(
|
|
"read_file",
|
|
"def process():\n data = load_data()\n result = analyze(data)\n return result",
|
|
),
|
|
create_tool_use_msg("read_file", {"path": "/app/utils.py"}),
|
|
create_tool_result_msg("read_file", "def load_data():\n return open('data.json').read()"),
|
|
create_tool_use_msg("run_command", {"command": "python /app/main.py"}),
|
|
create_tool_result_msg("run_command", "FileNotFoundError: [Errno 2] No such file or directory: 'data.json'"),
|
|
create_assistant_msg(
|
|
"I found the issue! The script is looking for 'data.json' "
|
|
"in the current directory instead of an absolute path.",
|
|
),
|
|
create_user_msg("How should I fix it?"),
|
|
create_assistant_msg(
|
|
"Update load_data() to use an absolute path:\n"
|
|
"```python\nimport os\ndef load_data():\n"
|
|
" script_dir = os.path.dirname(__file__)\n"
|
|
" return open(os.path.join(script_dir, 'data.json')).read()\n```",
|
|
),
|
|
]
|
|
|
|
result = asyncio.run(compactor.call(messages=messages))
|
|
|
|
assert result, "Result should not be empty"
|
|
assert isinstance(result, str), f"Result should be string, got: {type(result)}"
|
|
print_pass("test_conversation_with_multiple_tool_calls")
|
|
|
|
|
|
def test_low_threshold():
|
|
"""Test compaction with low memory threshold."""
|
|
compactor = Compactor(
|
|
memory_compact_threshold=500,
|
|
as_token_counter=get_token_counter(),
|
|
as_llm=get_dash_chat_model(),
|
|
as_llm_formatter=get_formatter(),
|
|
)
|
|
|
|
messages = [
|
|
create_user_msg("Build a web scraper."),
|
|
create_assistant_msg("I'll create a web scraper using BeautifulSoup and requests."),
|
|
create_user_msg("Make it handle pagination."),
|
|
create_assistant_msg("Here's the paginated scraper implementation with error handling."),
|
|
]
|
|
|
|
result = asyncio.run(compactor.call(messages=messages))
|
|
|
|
assert result, "Result should not be empty"
|
|
assert isinstance(result, str), f"Result should be string, got: {type(result)}"
|
|
print("test_low_threshold PASSED")
|
|
|
|
|
|
def test_high_threshold():
|
|
"""Test compaction with high memory threshold."""
|
|
compactor = Compactor(
|
|
memory_compact_threshold=10000,
|
|
as_token_counter=get_token_counter(),
|
|
as_llm=get_dash_chat_model(),
|
|
as_llm_formatter=get_formatter(),
|
|
)
|
|
|
|
messages = [
|
|
create_user_msg("Create a calculator class."),
|
|
create_assistant_msg("Here's a Calculator class with basic operations: add, subtract, multiply, divide."),
|
|
]
|
|
|
|
result = asyncio.run(compactor.call(messages=messages))
|
|
|
|
assert result, "Result should not be empty"
|
|
assert isinstance(result, str), f"Result should be string, got: {type(result)}"
|
|
print("test_high_threshold PASSED")
|
|
|
|
|
|
def run_all_tests():
|
|
"""Run all tests."""
|
|
tests = [
|
|
test_empty_messages,
|
|
test_short_conversation,
|
|
test_medium_conversation,
|
|
test_long_conversation,
|
|
test_with_previous_summary,
|
|
test_conversation_with_multiple_tool_calls,
|
|
test_low_threshold,
|
|
test_high_threshold,
|
|
]
|
|
|
|
passed = 0
|
|
failed = 0
|
|
|
|
for test in tests:
|
|
try:
|
|
print_test_header(test.__name__)
|
|
test()
|
|
passed += 1
|
|
except AssertionError as e:
|
|
print_fail(test.__name__, str(e))
|
|
failed += 1
|
|
except Exception as e:
|
|
print_error(test.__name__, str(e))
|
|
failed += 1
|
|
|
|
# 打印最终统计结果
|
|
print(f"\n{Colors.CYAN}{'=' * 60}{Colors.RESET}")
|
|
print(f"{Colors.BOLD}Test Results Summary{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'=' * 60}{Colors.RESET}")
|
|
print(f"{Colors.GREEN}{Colors.BOLD}✓ Passed: {passed}{Colors.RESET}")
|
|
if failed > 0:
|
|
print(f"{Colors.RED}{Colors.BOLD}✗ Failed: {failed}{Colors.RESET}")
|
|
else:
|
|
print(f"{Colors.GREEN}✗ Failed: {failed}{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{'=' * 60}{Colors.RESET}")
|
|
|
|
if failed == 0:
|
|
print(f"\n{Colors.GREEN}{Colors.BOLD}🎉 All tests passed!{Colors.RESET}")
|
|
else:
|
|
print(f"\n{Colors.RED}{Colors.BOLD}💥 Some tests failed!{Colors.RESET}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run_all_tests()
|