mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
feat: persist agent chat history
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
|
||||
from langchain_core.messages import HumanMessage
|
||||
|
||||
from app.agent import MoviePilotAgent
|
||||
from app.agent.memory import memory_manager
|
||||
from app.db.agentchat_oper import AgentChatOper
|
||||
|
||||
|
||||
def test_agent_chat_oper_saves_display_messages_with_channel():
|
||||
"""Agent 会话历史应保存展示消息与渠道标识。"""
|
||||
oper = AgentChatOper()
|
||||
oper.save_display_messages(
|
||||
session_id="session-chat",
|
||||
user_id="1",
|
||||
username="admin",
|
||||
channel="Telegram",
|
||||
source="telegram-main",
|
||||
original_chat_id="chat-1",
|
||||
messages=[
|
||||
{
|
||||
"id": "user-1",
|
||||
"role": "user",
|
||||
"content": "帮我看看下载器",
|
||||
"createdAt": 1,
|
||||
"status": "done",
|
||||
"tools": [],
|
||||
"attachments": [],
|
||||
"choices": [],
|
||||
}
|
||||
],
|
||||
)
|
||||
chat = AgentChatOper().get(session_id="session-chat", user_id="1")
|
||||
|
||||
assert chat.channel == "Telegram"
|
||||
assert chat.source == "telegram-main"
|
||||
assert chat.original_chat_id == "chat-1"
|
||||
assert chat.message_count == 1
|
||||
assert chat.title == "帮我看看下载器"
|
||||
|
||||
|
||||
def test_agent_chat_oper_keeps_generated_title_when_saving_display_messages():
|
||||
"""保存展示消息时不应覆盖已生成的模型标题。"""
|
||||
oper = AgentChatOper()
|
||||
oper.update_title_if_empty(
|
||||
session_id="session-title",
|
||||
user_id="1",
|
||||
username="admin",
|
||||
channel="WebAgent",
|
||||
source="web-agent",
|
||||
title="下载器状态排查",
|
||||
)
|
||||
oper.save_display_messages(
|
||||
session_id="session-title",
|
||||
user_id="1",
|
||||
messages=[
|
||||
{
|
||||
"id": "user-1",
|
||||
"role": "user",
|
||||
"content": "帮我看看下载器现在是不是正常",
|
||||
"createdAt": 1,
|
||||
"status": "done",
|
||||
"tools": [],
|
||||
"attachments": [],
|
||||
"choices": [],
|
||||
}
|
||||
],
|
||||
title="帮我看看下载器现在是不是正常",
|
||||
)
|
||||
|
||||
chat = AgentChatOper().get(session_id="session-title", user_id="1")
|
||||
summary = AgentChatOper.to_summary(chat)
|
||||
|
||||
assert chat.title == "下载器状态排查"
|
||||
assert "preview" not in summary
|
||||
assert "messages" not in summary
|
||||
|
||||
|
||||
def test_agent_prepare_chat_title_generates_title(monkeypatch):
|
||||
"""首次调用 Agent 时应使用模型生成会话标题并写入渠道信息。"""
|
||||
|
||||
class FakeTitleModel:
|
||||
"""测试用标题模型。"""
|
||||
|
||||
async def ainvoke(self, messages):
|
||||
"""返回固定标题。"""
|
||||
assert "标题生成器" in messages[0].content
|
||||
assert messages[1].content == "帮我看看下载器现在是不是正常"
|
||||
return SimpleNamespace(content="「下载器状态排查」")
|
||||
|
||||
async def fake_initialize_llm(self, streaming=False):
|
||||
"""返回测试标题模型。"""
|
||||
return FakeTitleModel()
|
||||
|
||||
monkeypatch.setattr(MoviePilotAgent, "_initialize_llm", fake_initialize_llm)
|
||||
agent = MoviePilotAgent(
|
||||
session_id="session-ai-title",
|
||||
user_id="3",
|
||||
channel="WebAgent",
|
||||
source="web-agent",
|
||||
username="admin",
|
||||
)
|
||||
|
||||
asyncio.run(agent.prepare_chat_title("帮我看看下载器现在是不是正常"))
|
||||
chat = AgentChatOper().get(session_id="session-ai-title", user_id="3")
|
||||
|
||||
assert chat.title == "下载器状态排查"
|
||||
assert chat.channel == "WebAgent"
|
||||
assert chat.source == "web-agent"
|
||||
|
||||
|
||||
def test_memory_manager_restores_agent_messages_from_database():
|
||||
"""内存缓存缺失时应从 Agent 会话历史表恢复原始 messages。"""
|
||||
session_id = "session-memory"
|
||||
user_id = "2"
|
||||
memory_manager.clear_memory(session_id, user_id)
|
||||
AgentChatOper().save_agent_messages(
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
messages=[
|
||||
{
|
||||
"type": "human",
|
||||
"data": {
|
||||
"content": "继续之前的话题",
|
||||
"additional_kwargs": {},
|
||||
"response_metadata": {},
|
||||
"type": "human",
|
||||
"name": None,
|
||||
"id": None,
|
||||
"example": False,
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
messages = memory_manager.get_agent_messages(session_id=session_id, user_id=user_id)
|
||||
|
||||
assert len(messages) == 1
|
||||
assert isinstance(messages[0], HumanMessage)
|
||||
assert messages[0].content == "继续之前的话题"
|
||||
@@ -8,6 +8,8 @@ from app.agent import ReplyMode
|
||||
from app.api.endpoints.agent import (
|
||||
_WebAgentMoviePilotAgent,
|
||||
_WEB_AGENT_FILE_REGISTRY,
|
||||
_apply_web_agent_display_event,
|
||||
_build_web_agent_input_attachments,
|
||||
_build_web_agent_notification_events,
|
||||
_build_web_agent_session_id,
|
||||
_prepare_web_agent_audio_attachment_path,
|
||||
@@ -16,6 +18,7 @@ from app.api.endpoints.agent import (
|
||||
_resolve_web_agent_choice_payload,
|
||||
_split_web_agent_output,
|
||||
)
|
||||
from app.db.agentchat_oper import AgentChatOper
|
||||
from app.helper.interaction import AgentInteractionOption, agent_interaction_manager
|
||||
from app.schemas.message import ChannelCapability, ChannelCapabilityManager
|
||||
from app.schemas.types import MessageChannel, NotificationType
|
||||
@@ -74,6 +77,73 @@ def test_build_web_agent_session_id_is_stable_per_user_and_seed():
|
||||
assert first.startswith("web-agent:")
|
||||
|
||||
|
||||
def test_build_web_agent_session_id_reuses_accessible_history():
|
||||
"""传入已有历史会话 ID 时应直接复用,避免跨渠道继续对话丢上下文。"""
|
||||
user = SimpleNamespace(id=1, name="admin", is_superuser=True)
|
||||
AgentChatOper().save_display_messages(
|
||||
session_id="telegram-session",
|
||||
user_id="telegram-user",
|
||||
username="tester",
|
||||
channel=MessageChannel.Telegram.value,
|
||||
source="telegram-main",
|
||||
messages=[],
|
||||
title="Telegram 会话",
|
||||
)
|
||||
|
||||
assert _build_web_agent_session_id(user, "telegram-session") == "telegram-session"
|
||||
|
||||
|
||||
def test_apply_web_agent_display_event_updates_snapshot():
|
||||
"""WebAgent SSE 事件应可聚合为服务端展示快照。"""
|
||||
message = {
|
||||
"id": "assistant-1",
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"createdAt": 1,
|
||||
"status": "streaming",
|
||||
"tools": [],
|
||||
"attachments": [],
|
||||
"choices": [],
|
||||
}
|
||||
|
||||
_apply_web_agent_display_event({"type": "delta", "content": "你好"}, message)
|
||||
_apply_web_agent_display_event({"type": "tool", "message": "查询订阅"}, message)
|
||||
_apply_web_agent_display_event(
|
||||
{
|
||||
"type": "attachment",
|
||||
"attachment": {"kind": "file", "url": "message/agent/file/a"},
|
||||
},
|
||||
message,
|
||||
)
|
||||
_apply_web_agent_display_event({"type": "done"}, message)
|
||||
|
||||
assert message["content"] == "你好"
|
||||
assert message["status"] == "done"
|
||||
assert len(message["tools"]) == 1
|
||||
assert message["tools"][0]["message"] == "查询订阅"
|
||||
assert message["tools"][0]["status"] == "done"
|
||||
assert message["attachments"] == [{"kind": "file", "url": "message/agent/file/a"}]
|
||||
|
||||
|
||||
def test_build_web_agent_input_attachments_marks_kinds():
|
||||
"""WebAgent 用户输入附件应转换为可展示的附件记录。"""
|
||||
attachments = _build_web_agent_input_attachments(
|
||||
images=["data:image/png;base64,abc"],
|
||||
files=[
|
||||
{
|
||||
"ref": "message/agent/file/file-1",
|
||||
"name": "report.txt",
|
||||
"mime_type": "text/plain",
|
||||
"size": 5,
|
||||
}
|
||||
],
|
||||
audio_refs=["message/agent/file/audio-1"],
|
||||
)
|
||||
|
||||
assert [item["kind"] for item in attachments] == ["image", "file", "audio"]
|
||||
assert attachments[1]["name"] == "report.txt"
|
||||
|
||||
|
||||
def test_web_agent_admin_context_uses_current_user_id():
|
||||
"""Web Agent 工具权限应按当前登录用户 ID 判断管理员身份。"""
|
||||
agent = _WebAgentMoviePilotAgent(
|
||||
|
||||
Reference in New Issue
Block a user