fix(exploration): flatten dict reply payloads instead of storing str(dict)

E2E on t480 showed assistant bubbles rendering {'content': '...'} because
tutu replies carry msgBody as a parsed object and the router stored
str(reply.content). Coerce to the inner text before persisting.
This commit is contained in:
sinohqb 2026-08-04 02:26:24 +08:00
parent 958cefc380
commit 9abf572949
2 changed files with 47 additions and 1 deletions

View File

@ -10,6 +10,7 @@ with 409 plus a readable reason, so the rejection itself is feedback to the
resident agent.
"""
import json
from datetime import datetime
from typing import Any
@ -61,6 +62,20 @@ class CloseSessionRequest(BaseModel):
experience: dict[str, Any]
def _coerce_reply_text(content: Any) -> str:
"""Flatten a reply payload to text; tutu returns msgBody as a parsed object, and str(dict) would leak a Python repr into the view."""
if isinstance(content, str):
return content
if isinstance(content, dict):
for key in ("content", "text", "message"):
value = content.get(key)
if isinstance(value, str) and value:
return value
if content is None:
return ""
return json.dumps(content, ensure_ascii=False)
def _check_creation_guardrails(
campaign: Campaign,
triggered_by: ExplorationTrigger,
@ -283,7 +298,7 @@ async def send_session_message(
received_at = utc_now()
latency_ms = int((received_at - sent_at).total_seconds() * 1000)
reply_text = str(reply.content)
reply_text = _coerce_reply_text(reply.content)
message_repo.save_message(
ExplorationMessage(
session_id=session_obj.id,

View File

@ -175,6 +175,37 @@ async def test_full_lifecycle_create_message_close(seeded_db, mock_channel, clie
assert closed["closed_at"] is not None
async def test_dict_reply_content_is_flattened_to_text(seeded_db, monkeypatch, client):
"""通道回复 content 为对象(如 tutu msgBody时应提取文本而非存 str(dict)。"""
from agenteval.channels.base import Reply
from tests.unit.mock_channel import MockChannel
class _DictReplyChannel(MockChannel):
async def poll_reply(self, question_msg_id, timeout=30.0, poll_interval=1.0):
return Reply(
question_msg_id=question_msg_id,
content={"content": "您好,我是客服"},
raw_message={},
)
_stub_channel_factory(monkeypatch, _DictReplyChannel(reply_text="unused"))
resp = await _create_session(client)
assert resp.status_code == 200, resp.text
session_id = resp.json()["id"]
resp = await client.post(
f"/api/exploration/sessions/{session_id}/messages",
json={"content": "你好"},
)
assert resp.status_code == 200, resp.text
assert resp.json()["reply"] == "您好,我是客服"
messages_resp = await client.get(f"/api/exploration/sessions/{session_id}/messages")
assistant = [m for m in messages_resp.json()["messages"] if m["role"] == "assistant"]
assert assistant[0]["content"] == "您好,我是客服"
async def test_create_requires_existing_running_campaign(seeded_db, client):
resp = await _create_session(client, campaign_id="nope")
assert resp.status_code == 404