rag-feedback captures explicit feedback and implicit signals about a query,
PII-redacts free-text comments, persists them tenant-scoped, and aggregates them
into a per-tenant dashboard.
| Piece | Where |
|---|---|
| Recorder (normalise → redact → store → emit) | rag_feedback.FeedbackRecorder |
| Aggregator (pure) | rag_feedback.aggregate_feedback |
| Score / kind helpers | rag_feedback.score_for_signal / kind_for_signal |
| Domain types | rag_core.types (FeedbackRecord / FeedbackStats / FeedbackKind / FeedbackSignal) |
| Store SPI + noop | rag_core.spi.FeedbackStore / rag_core.spi.noop.NoopFeedbackStore |
| Wire types | rag_core.gateway_types (FeedbackRequest / FeedbackAck) |
| Events | rag_observability.events (FeedbackEvent, feedback.recorded, feedback.record_degraded) |
| Config | cfg.feedback (rag_config.FeedbackConfig) |
| Gateway routes | POST /v1/feedback, GET /v1/status/feedback |
| CLI | ragctl feedback |
A submission carries one FeedbackSignal. kind (explicit / implicit) is
inferred from it when omitted, and a [-1, 1] score is derived:
| Signal | Kind | Score |
|---|---|---|
thumb_up |
explicit | +1 |
thumb_down |
explicit | −1 |
rating (1–5) |
explicit | (rating−3)/2 → −1…+1 |
comment |
explicit | 0 |
citation_click |
implicit | +1 |
answer_copied |
implicit | +1 |
accepted |
implicit | +1 |
regenerated |
implicit | −1 |
dwell |
implicit | 0 (raw value in value) |
Body identity (tenant_id / principal_id) like /v1/query; request_id is the
query being rated.
# explicit thumbs up
curl -s localhost:8000/v1/feedback -H 'content-type: application/json' -d '{
"tenant_id": "acme", "principal_id": "alice", "request_id": "q-123", "signal": "thumb_up"
}'
# rating + comment (the comment is PII-redacted before storage)
curl -s localhost:8000/v1/feedback -d '{
"tenant_id":"acme","principal_id":"alice","request_id":"q-123",
"signal":"rating","rating":5,"comment":"great, but reach me at a@b.com"
}'
# implicit signal — a user clicked a citation
curl -s localhost:8000/v1/feedback -d '{
"tenant_id":"acme","principal_id":"alice","request_id":"q-123",
"signal":"citation_click","target_chunk_id":"c-7"
}'The response is a FeedbackAck — {feedback_id, request_id, kind, signal, score, stored, comment_redacted}. stored=false (with feedback_id=null) when feedback
is disabled on the gateway or recording degraded; the submission is acknowledged
either way (recording is degrade-open). Invalid signal / out-of-range rating
→ 422.
Per-tenant aggregate. Tenant resolves from the tenant_id query param, else the
X-Tenant-Id header, else "default". Returns a FeedbackStats:
curl -s 'localhost:8000/v1/status/feedback?tenant_id=acme'
# {"tenant_id":"acme","total":12,"explicit_n":7,"implicit_n":5,
# "positive_n":9,"negative_n":2,"neutral_n":1,"comment_n":4,
# "citation_click_n":3,"regenerated_n":2,"mean_score":0.58,
# "by_signal":{"thumb_up":5,"rating":2,...}}ragctl feedback q-42 --tenant acme # record explicit + implicit, print the aggregate
ragctl feedback q-42 --no-redact # store the comment without redactionfeedback:
enabled: true # POST /v1/feedback live; GET /v1/status/feedback served
redact_comments: true # run the wired PIIDetector over comments before storage
max_comment_chars: 2000 # comments truncated to this length
max_records: 10000 # in-memory bound on the default store- Recorder.
record()normalises the score, redacts the comment (when a detector is wired andredact_comments), builds aFeedbackRecord, stores it, and emitsfeedback.recorded— all degrade-open (a store / redaction failure logsfeedback.record_degradedand returnsNone). - Privacy. The default gateway wires a pass-through
NoopPIIDetector; production injects a real one.comment_redactedrecords whether a redactor ran. The event never carries the comment text. - Aggregation.
aggregate_feedback(tenant_id, records)is pure and backend-agnostic; the dashboard calls it overFeedbackStore.list(...).
- Real PII detector — inject a Presidio-backed
PIIDetectorintoFeedbackRecorder(the gateway wiresNoopPIIDetectorby default). - Durable store — implement
FeedbackStore(e.g. Postgres) for cross-worker aggregation; the noop in-memory store is the default. - New signal — add a
FeedbackSignalmember + its polarity inrag_feedback.recorder._SIGNAL_POLARITY.
- ADR-0029 — decisions
- architecture/online-feedback.md — design
- reference/provenance.md — the Step 5.1 template