mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-17 09:50:31 +08:00
* refactor(ltm): redesign long-term memory with context compaction - Add raw_records / contexts / summaries data model per group - Add LLM summary compaction strategy alongside truncation - Add turn-based (_split_into_rounds) granularity - Add image caption integration into LTM history - Add tool_call / tool_result persistence into raw_records - Add active reply support driven by LTM state - Improve summary injection prefix with system note and delimiters - Add info-level logging for summary compaction lifecycle - Clarify default summary prompt with explicit preserve/drop rules - Add context_guard for history overflow protection in agent runner - Add internal agent history compaction in agent_sub_stages - Add comprehensive LTM unit tests and compaction test suites * fix(ltm): handle malformed JSON in tool args and clean up lock on session removal * fix(ltm): guard against duplicate system prompt note injection * fix(ltm): fall back to user message when internal marker parsing fails - Treat lines starting with <T:CALL>, <T:RES, or <BOT/ as regular user messages when their respective parsers return None, instead of silently dropping them. Defensive guard against malformed internal markers. * fix(ltm): release session lock during LLM summary generation * fix(ltm): trim raw_records in handle_message to prevent unbounded growth * perf(ltm): use len(s) instead of len(s.encode()) in trim loop Avoid allocating a new bytes object for every string when calculating buffer size in _trim_raw_records. Character count is sufficient for the approximate memory cap. * feat(ltm): make user segment truncation limits configurable * feat(ltm): pre-fill default LTM summary prompt in config and i18n * refactor(ltm): hardcode internal segment/trim constants * refactor(ltm): unify compaction strategy with main agent runner * feat(ltm): add @mention weight marker for group chat messages * test: fix test failures from LTM compaction unification * chore(dashboard): remove obsolete LTM compaction i18n metadata * chore: shrink codebase * feat(group-chat): implement group chat context management and related functionality --------- Co-authored-by: Tsukumi <112180165+Tsukumi233@users.noreply.github.com> Co-authored-by: zenfun <zenfun510@gmail.com> Co-authored-by: Soulter <905617992@qq.com>
121 lines
3.9 KiB
Python
121 lines
3.9 KiB
Python
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from astrbot.api.message_components import Plain
|
|
from astrbot.builtin_stars.astrbot.main import Main
|
|
|
|
|
|
def make_main_with_conversation_manager(conv_mgr):
|
|
main = Main.__new__(Main)
|
|
main.context = MagicMock()
|
|
main.context.conversation_manager = conv_mgr
|
|
return main
|
|
|
|
|
|
def make_event(umo: str = "aiocqhttp:GroupMessage:user_123_group_456"):
|
|
event = MagicMock()
|
|
event.unified_msg_origin = umo
|
|
event.get_platform_id.return_value = "aiocqhttp"
|
|
event.message_obj = SimpleNamespace(message=[Plain("hello")])
|
|
event.message_str = "hello"
|
|
event.session_id = "session-1"
|
|
return event
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_active_reply_does_not_create_conversation_when_current_missing():
|
|
conv_mgr = SimpleNamespace(
|
|
get_curr_conversation_id=AsyncMock(return_value=None),
|
|
new_conversation=AsyncMock(),
|
|
get_conversation=AsyncMock(),
|
|
)
|
|
main = make_main_with_conversation_manager(conv_mgr)
|
|
main.context.get_config.return_value = {
|
|
"provider_ltm_settings": {
|
|
"group_icl_enable": False,
|
|
"active_reply": {"enable": True},
|
|
},
|
|
}
|
|
main.context.get_using_provider.return_value = object()
|
|
main.group_chat_context = SimpleNamespace(
|
|
need_active_reply=AsyncMock(return_value=True),
|
|
handle_message=AsyncMock(),
|
|
)
|
|
event = make_event()
|
|
|
|
results = [item async for item in main.on_message(event)]
|
|
|
|
assert results == []
|
|
conv_mgr.get_curr_conversation_id.assert_awaited_once_with(event.unified_msg_origin)
|
|
conv_mgr.new_conversation.assert_not_called()
|
|
conv_mgr.get_conversation.assert_not_called()
|
|
event.request_llm.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_active_reply_reuses_current_umo_conversation():
|
|
conv = SimpleNamespace(cid="cid-1")
|
|
conv_mgr = SimpleNamespace(
|
|
get_curr_conversation_id=AsyncMock(return_value="cid-1"),
|
|
new_conversation=AsyncMock(),
|
|
get_conversation=AsyncMock(return_value=conv),
|
|
)
|
|
main = make_main_with_conversation_manager(conv_mgr)
|
|
main.context.get_config.return_value = {
|
|
"provider_ltm_settings": {
|
|
"group_icl_enable": False,
|
|
"active_reply": {"enable": True},
|
|
},
|
|
}
|
|
main.context.get_using_provider.return_value = object()
|
|
main.group_chat_context = SimpleNamespace(
|
|
need_active_reply=AsyncMock(return_value=True),
|
|
handle_message=AsyncMock(),
|
|
)
|
|
event = make_event("aiocqhttp:GroupMessage:user_999_group_456")
|
|
llm_request = object()
|
|
event.request_llm.return_value = llm_request
|
|
|
|
results = [item async for item in main.on_message(event)]
|
|
|
|
assert results == [llm_request]
|
|
conv_mgr.get_curr_conversation_id.assert_awaited_once_with(event.unified_msg_origin)
|
|
conv_mgr.new_conversation.assert_not_called()
|
|
conv_mgr.get_conversation.assert_awaited_once_with(
|
|
event.unified_msg_origin,
|
|
"cid-1",
|
|
)
|
|
event.request_llm.assert_called_once_with(
|
|
prompt="hello",
|
|
session_id="session-1",
|
|
image_urls=[],
|
|
conversation=conv,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_on_message_does_not_clear_group_context_on_first_enabled_message():
|
|
main = Main.__new__(Main)
|
|
main.context = MagicMock()
|
|
main.context.get_config.return_value = {
|
|
"provider_ltm_settings": {
|
|
"group_icl_enable": True,
|
|
"active_reply": {"enable": False},
|
|
},
|
|
}
|
|
main.group_chat_context = SimpleNamespace(
|
|
need_active_reply=AsyncMock(return_value=False),
|
|
handle_message=AsyncMock(),
|
|
remove_session=AsyncMock(),
|
|
)
|
|
event = make_event()
|
|
|
|
async for _ in main.on_message(event):
|
|
pass
|
|
|
|
main.group_chat_context.need_active_reply.assert_awaited_once_with(event)
|
|
main.group_chat_context.handle_message.assert_awaited_once_with(event)
|
|
main.group_chat_context.remove_session.assert_not_called()
|