mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-08 17:08:35 +08:00
fix(agent): persist streamed message order
This commit is contained in:
@@ -318,13 +318,19 @@ class MoviePilotAgent:
|
||||
"""
|
||||
构造可展示的 Agent 会话消息。
|
||||
"""
|
||||
normalized_content = content or ""
|
||||
return {
|
||||
"id": f"{role}-{uuid.uuid4().hex}",
|
||||
"role": role,
|
||||
"content": content or "",
|
||||
"content": normalized_content,
|
||||
"createdAt": cls._current_timestamp_ms(),
|
||||
"status": status,
|
||||
"tools": [],
|
||||
"segments": (
|
||||
[{"type": "text", "content": normalized_content}]
|
||||
if normalized_content
|
||||
else []
|
||||
),
|
||||
"attachments": attachments or [],
|
||||
"choices": [],
|
||||
}
|
||||
|
||||
@@ -318,16 +318,53 @@ async def _get_accessible_agent_chat(
|
||||
return chat
|
||||
|
||||
|
||||
def _append_web_agent_text_segment(assistant_message: dict, content: str) -> None:
|
||||
"""
|
||||
将文本增量追加到展示消息,并仅合并相邻文本片段。
|
||||
|
||||
:param assistant_message: 当前助手展示消息
|
||||
:param content: 新增文本
|
||||
"""
|
||||
if not content:
|
||||
return
|
||||
assistant_message["content"] = str(assistant_message.get("content") or "") + content
|
||||
segments = assistant_message.setdefault("segments", [])
|
||||
if segments and segments[-1].get("type") == "text":
|
||||
segments[-1]["content"] = str(segments[-1].get("content") or "") + content
|
||||
else:
|
||||
segments.append({"type": "text", "content": content})
|
||||
|
||||
|
||||
def _build_legacy_web_agent_segments(content: str, tools: list[dict]) -> list[dict]:
|
||||
"""
|
||||
为未携带有序片段的旧展示消息生成兼容布局。
|
||||
|
||||
:param content: 聚合后的助手文本
|
||||
:param tools: 工具提示列表
|
||||
:return: 按旧版工具在前、文本在后的顺序生成的片段
|
||||
"""
|
||||
segments = [
|
||||
{"type": "tool", "toolIndex": index}
|
||||
for index in range(len(tools))
|
||||
]
|
||||
if content:
|
||||
segments.append({"type": "text", "content": content})
|
||||
return segments
|
||||
|
||||
|
||||
def _apply_web_agent_display_event(event: dict, assistant_message: dict) -> None:
|
||||
"""
|
||||
将 WebAgent SSE 事件同步应用到服务端展示消息快照。
|
||||
"""
|
||||
event_type = event.get("type")
|
||||
if event_type == "delta":
|
||||
assistant_message["content"] += event.get("content") or ""
|
||||
_append_web_agent_text_segment(
|
||||
assistant_message, event.get("content") or ""
|
||||
)
|
||||
elif event_type == "tool":
|
||||
for tool in assistant_message["tools"]:
|
||||
tool["status"] = "done"
|
||||
tool_index = len(assistant_message["tools"])
|
||||
assistant_message["tools"].append(
|
||||
{
|
||||
"id": f"tool-{uuid.uuid4().hex}",
|
||||
@@ -335,6 +372,9 @@ def _apply_web_agent_display_event(event: dict, assistant_message: dict) -> None
|
||||
"status": "running",
|
||||
}
|
||||
)
|
||||
assistant_message.setdefault("segments", []).append(
|
||||
{"type": "tool", "toolIndex": tool_index}
|
||||
)
|
||||
elif event_type == "attachment" and event.get("attachment"):
|
||||
assistant_message["attachments"].append(event["attachment"])
|
||||
elif event_type == "choice" and event.get("choice"):
|
||||
@@ -346,13 +386,21 @@ def _apply_web_agent_display_event(event: dict, assistant_message: dict) -> None
|
||||
assistant_message["attachments"] = target_message.get("attachments") or []
|
||||
assistant_message["choices"] = target_message.get("choices") or []
|
||||
assistant_message["tools"] = target_message.get("tools") or []
|
||||
target_segments = target_message.get("segments")
|
||||
assistant_message["segments"] = (
|
||||
target_segments
|
||||
if isinstance(target_segments, list)
|
||||
else _build_legacy_web_agent_segments(
|
||||
assistant_message["content"], assistant_message["tools"]
|
||||
)
|
||||
)
|
||||
assistant_message["status"] = target_message.get("status") or "done"
|
||||
elif event_type == "error":
|
||||
assistant_message["status"] = "error"
|
||||
assistant_message["content"] = (
|
||||
assistant_message["content"]
|
||||
or event.get("message")
|
||||
or "智能助手响应失败"
|
||||
if not assistant_message["content"]:
|
||||
_append_web_agent_text_segment(
|
||||
assistant_message,
|
||||
event.get("message") or "智能助手响应失败",
|
||||
)
|
||||
for tool in assistant_message["tools"]:
|
||||
tool["status"] = "done"
|
||||
|
||||
@@ -133,6 +133,16 @@ class AgentChatToolCall(BaseModel):
|
||||
status: str = Field(default="done", description="工具状态")
|
||||
|
||||
|
||||
class AgentChatMessageSegment(BaseModel):
|
||||
"""
|
||||
Agent 会话消息中的有序展示片段。
|
||||
"""
|
||||
|
||||
type: str = Field(..., description="片段类型")
|
||||
content: str = Field(default="", description="文本片段内容")
|
||||
toolIndex: Optional[int] = Field(None, description="工具提示索引")
|
||||
|
||||
|
||||
class AgentChatChoiceButton(BaseModel):
|
||||
"""
|
||||
Agent 会话选择按钮。
|
||||
@@ -185,6 +195,7 @@ class AgentChatMessage(BaseModel):
|
||||
createdAt: Union[int, float] = Field(..., description="创建时间戳")
|
||||
status: str = Field(default="done", description="消息状态")
|
||||
tools: list[AgentChatToolCall] = Field(default_factory=list, description="工具提示列表")
|
||||
segments: list[AgentChatMessageSegment] = Field(default_factory=list, description="有序展示片段")
|
||||
attachments: list[AgentChatAttachment] = Field(default_factory=list, description="附件列表")
|
||||
choices: list[AgentChatChoiceCard] = Field(default_factory=list, description="选择卡片列表")
|
||||
choice_selection: Optional[AgentChatChoiceSelection] = Field(None, description="用户选择项快照")
|
||||
|
||||
@@ -106,7 +106,7 @@ def test_build_web_agent_session_id_reuses_accessible_history():
|
||||
|
||||
|
||||
def test_apply_web_agent_display_event_updates_snapshot():
|
||||
"""WebAgent SSE 事件应可聚合为服务端展示快照。"""
|
||||
"""WebAgent SSE 事件应按到达顺序聚合为服务端展示快照。"""
|
||||
message = {
|
||||
"id": "assistant-1",
|
||||
"role": "assistant",
|
||||
@@ -114,12 +114,14 @@ def test_apply_web_agent_display_event_updates_snapshot():
|
||||
"createdAt": 1,
|
||||
"status": "streaming",
|
||||
"tools": [],
|
||||
"segments": [],
|
||||
"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": "delta", "content": ",查询完成"}, message)
|
||||
_apply_web_agent_display_event(
|
||||
{
|
||||
"type": "attachment",
|
||||
@@ -129,14 +131,48 @@ def test_apply_web_agent_display_event_updates_snapshot():
|
||||
)
|
||||
_apply_web_agent_display_event({"type": "done"}, message)
|
||||
|
||||
assert message["content"] == "你好"
|
||||
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["segments"] == [
|
||||
{"type": "text", "content": "你好"},
|
||||
{"type": "tool", "toolIndex": 0},
|
||||
{"type": "text", "content": ",查询完成"},
|
||||
]
|
||||
assert message["attachments"] == [{"kind": "file", "url": "message/agent/file/a"}]
|
||||
|
||||
|
||||
def test_agent_chat_display_schema_preserves_ordered_segments():
|
||||
"""前端回传会话快照时应保留文字和工具的有序片段。"""
|
||||
payload = schemas.AgentChatDisplaySaveRequest(
|
||||
messages=[
|
||||
{
|
||||
"id": "assistant-1",
|
||||
"role": "assistant",
|
||||
"content": "先检查检查完成",
|
||||
"createdAt": 1,
|
||||
"status": "done",
|
||||
"tools": [
|
||||
{"id": "tool-1", "message": "执行检查", "status": "done"}
|
||||
],
|
||||
"segments": [
|
||||
{"type": "text", "content": "先检查"},
|
||||
{"type": "tool", "toolIndex": 0},
|
||||
{"type": "text", "content": "检查完成"},
|
||||
],
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
assert payload.messages[0].model_dump()["segments"] == [
|
||||
{"type": "text", "content": "先检查", "toolIndex": None},
|
||||
{"type": "tool", "content": "", "toolIndex": 0},
|
||||
{"type": "text", "content": "检查完成", "toolIndex": None},
|
||||
]
|
||||
|
||||
|
||||
def test_build_web_agent_input_attachments_marks_kinds():
|
||||
"""WebAgent 用户输入附件应转换为可展示的附件记录。"""
|
||||
attachments = _build_web_agent_input_attachments(
|
||||
|
||||
Reference in New Issue
Block a user