feat(persistence): add user feedback + follow-up run association

Phase 2-C: feedback and follow-up tracking.

- FeedbackRow ORM model (rating +1/-1, optional message_id, comment)
- FeedbackRepository with CRUD, list_by_run/thread, aggregate stats
- Feedback API endpoints: create, list, stats, delete
- follow_up_to_run_id in RunCreateRequest (explicit or auto-detected
  from latest successful run on the thread)
- Worker writes follow_up_to_run_id into human_message event metadata
- Gateway deps: feedback_repo factory + getter
- 17 new tests (14 FeedbackRepository + 3 follow-up association)
- 109 total tests pass, zero regressions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
rayhpeng
2026-04-02 19:10:11 +08:00
parent e3179cd54d
commit 5cb0471af5
11 changed files with 508 additions and 3 deletions
+23
View File
@@ -46,6 +46,9 @@ async def langgraph_runtime(app: FastAPI) -> AsyncGenerator[None, None]:
# Initialize run event store based on config
app.state.run_event_store = _make_run_event_store(config)
# Initialize feedback repository (None when no DB engine)
app.state.feedback_repo = _make_feedback_repo()
# RunManager with store backing for persistence
app.state.run_manager = RunManager(store=app.state.run_store)
@@ -74,6 +77,18 @@ def _make_run_store() -> RunStore:
return MemoryRunStore()
def _make_feedback_repo():
"""Create a FeedbackRepository if DB engine is available, else None."""
from deerflow.persistence.engine import get_session_factory
sf = get_session_factory()
if sf is not None:
from deerflow.persistence.repositories.feedback_repo import FeedbackRepository
return FeedbackRepository(sf)
return None
def _make_run_event_store(config) -> RunEventStore:
from deerflow.runtime.events.store import make_run_event_store
@@ -123,6 +138,14 @@ def get_run_event_store(request: Request) -> RunEventStore:
return store
def get_feedback_repo(request: Request):
"""Return the FeedbackRepository, or 503 if not available."""
repo = getattr(request.app.state, "feedback_repo", None)
if repo is None:
raise HTTPException(status_code=503, detail="Feedback not available")
return repo
def get_run_store(request: Request) -> RunStore:
"""Return the RunStore, or 503 if not available."""
store = getattr(request.app.state, "run_store", None)