mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-28 17:06:05 +00:00
Rebase the feedback migration onto main's new chain tip again: main added 0009_webhook_dedupe (also chained after 0008_thread_operation_kind), so 0009_feedback_tags becomes 0010_feedback_tags with down_revision=0009_webhook_dedupe, keeping the chain linear. Conflicts resolved: - Five head-pin tests take main's version with the pin bumped to 0010_feedback_tags. - test_thread_messages_page.py keeps this branch's feedback_service wiring over main's feedback_repo wiring, but stubs both service methods so the thread-grouped path main added coverage for stays stubbed (latest_per_run_in_thread alongside latest_for_runs). - test_thread_messages_feedback.py keeps both sides' imports; each is used (Feedback for the fixture, EditReplayVisibility for the run manager stub). Verified: 79 tests across the conflicted files plus the migration and bootstrap suites, and 54 feedback tests, all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
87 lines
3.4 KiB
Python
87 lines
3.4 KiB
Python
"""Regression for the thread-messages feedback attachment.
|
|
|
|
GET /api/threads/{thread_id}/messages attaches user feedback to the last AI
|
|
message of each run. AI messages are stored by RunJournal with event_type
|
|
"llm.ai.response"; the endpoint previously matched the non-existent
|
|
"ai_message", so feedback was never attached and the grouped-feedback query
|
|
ran on every request for nothing.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from _router_auth_helpers import make_authed_test_app
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.gateway.routers import thread_runs
|
|
from deerflow.domain.feedback import Feedback
|
|
from deerflow.runtime.runs.manager import EditReplayVisibility
|
|
|
|
|
|
def _make_app(messages, feedback_grouped):
|
|
app = make_authed_test_app()
|
|
app.include_router(thread_runs.router)
|
|
|
|
event_store = MagicMock()
|
|
event_store.list_messages = AsyncMock(return_value=messages)
|
|
app.state.run_event_store = event_store
|
|
|
|
feedback_service = MagicMock()
|
|
feedback_service.latest_per_run_in_thread = AsyncMock(return_value=feedback_grouped)
|
|
app.state.feedback_service = feedback_service
|
|
|
|
# list_thread_messages also calls run_manager.list_by_thread to inject
|
|
# turn durations; stub it to return no runs so that path stays inert.
|
|
run_manager = MagicMock()
|
|
run_manager.list_successful_regenerate_sources = AsyncMock(return_value=set())
|
|
run_manager.list_edit_replay_visibility = AsyncMock(return_value=EditReplayVisibility())
|
|
run_manager.list_by_thread = AsyncMock(return_value=[])
|
|
app.state.run_manager = run_manager
|
|
|
|
return app, feedback_service
|
|
|
|
|
|
def _ai(run_id: str, seq: int, content: str) -> dict:
|
|
return {"seq": seq, "run_id": run_id, "event_type": "llm.ai.response", "category": "message", "content": content}
|
|
|
|
|
|
def _human(run_id: str, seq: int) -> dict:
|
|
return {"seq": seq, "run_id": run_id, "event_type": "llm.human.input", "category": "message", "content": "hi"}
|
|
|
|
|
|
def test_feedback_attached_to_last_ai_message_per_run():
|
|
messages = [
|
|
_human("r1", 1),
|
|
_ai("r1", 2, "first"),
|
|
_ai("r1", 3, "final answer"), # last AI of r1 -> should get feedback
|
|
_ai("r2", 4, "other run"), # last AI of r2 -> no feedback row
|
|
]
|
|
grouped = {"r1": Feedback(feedback_id="fb-1", run_id="r1", thread_id="t1", rating=1, comment="nice")}
|
|
app, feedback_service = _make_app(messages, grouped)
|
|
|
|
resp = TestClient(app).get("/api/threads/t1/messages")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
|
|
by_seq = {m["seq"]: m for m in data}
|
|
# The bug: this used to be None for every message.
|
|
assert by_seq[3]["feedback"] == {"feedback_id": "fb-1", "rating": 1, "comment": "nice", "tags": []}
|
|
# Earlier AI message of the same run and the human message get no feedback.
|
|
assert by_seq[2]["feedback"] is None
|
|
assert by_seq[1]["feedback"] is None
|
|
# r2's last AI message has no feedback row.
|
|
assert by_seq[4]["feedback"] is None
|
|
feedback_service.latest_per_run_in_thread.assert_awaited_once()
|
|
|
|
|
|
def test_no_feedback_query_when_thread_has_no_ai_message():
|
|
messages = [_human("r1", 1)]
|
|
app, feedback_service = _make_app(messages, {})
|
|
|
|
resp = TestClient(app).get("/api/threads/t1/messages")
|
|
assert resp.status_code == 200
|
|
assert resp.json()[0]["feedback"] is None
|
|
# No AI message -> the grouped feedback query must not run.
|
|
feedback_service.latest_per_run_in_thread.assert_not_awaited()
|