1use crate::Result;
10use async_trait::async_trait;
11use serde::{Deserialize, Serialize};
12use std::sync::Arc;
13use std::time::{SystemTime, UNIX_EPOCH};
14
15fn timestamp() -> f64 {
16 SystemTime::now()
17 .duration_since(UNIX_EPOCH)
18 .map(|d| d.as_secs_f64())
19 .unwrap_or(0.0)
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ChoiceSelectionFeedback {
25 pub request_id: String,
26 pub chosen_index: u32,
27 pub rejected_indices: Option<Vec<u32>>,
28 pub latency_to_select_ms: Option<u64>,
29 pub ui_context: Option<serde_json::Value>,
30 pub candidate_hashes: Option<Vec<String>>,
31 pub timestamp: f64,
32}
33
34impl ChoiceSelectionFeedback {
35 pub fn new(request_id: impl Into<String>, chosen_index: u32) -> Self {
36 Self {
37 request_id: request_id.into(),
38 chosen_index,
39 rejected_indices: None,
40 latency_to_select_ms: None,
41 ui_context: None,
42 candidate_hashes: None,
43 timestamp: timestamp(),
44 }
45 }
46 pub fn with_rejected(mut self, indices: Vec<u32>) -> Self {
47 self.rejected_indices = Some(indices);
48 self
49 }
50 pub fn with_latency(mut self, ms: u64) -> Self {
51 self.latency_to_select_ms = Some(ms);
52 self
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct RatingFeedback {
59 pub request_id: String,
60 pub rating: u32,
61 pub max_rating: u32,
62 pub category: Option<String>,
63 pub comment: Option<String>,
64 pub timestamp: f64,
65}
66impl RatingFeedback {
67 pub fn new(request_id: impl Into<String>, rating: u32) -> Self {
68 Self {
69 request_id: request_id.into(),
70 rating,
71 max_rating: 5,
72 category: None,
73 comment: None,
74 timestamp: timestamp(),
75 }
76 }
77 pub fn with_max_rating(mut self, m: u32) -> Self {
78 self.max_rating = m;
79 self
80 }
81 pub fn with_comment(mut self, c: impl Into<String>) -> Self {
82 self.comment = Some(c.into());
83 self
84 }
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct ThumbsFeedback {
90 pub request_id: String,
91 pub is_positive: bool,
92 pub reason: Option<String>,
93 pub timestamp: f64,
94}
95impl ThumbsFeedback {
96 pub fn thumbs_up(request_id: impl Into<String>) -> Self {
97 Self {
98 request_id: request_id.into(),
99 is_positive: true,
100 reason: None,
101 timestamp: timestamp(),
102 }
103 }
104 pub fn thumbs_down(request_id: impl Into<String>) -> Self {
105 Self {
106 request_id: request_id.into(),
107 is_positive: false,
108 reason: None,
109 timestamp: timestamp(),
110 }
111 }
112 pub fn with_reason(mut self, r: impl Into<String>) -> Self {
113 self.reason = Some(r.into());
114 self
115 }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct TextFeedback {
121 pub request_id: String,
122 pub text: String,
123 pub category: Option<String>,
124 pub timestamp: f64,
125}
126impl TextFeedback {
127 pub fn new(request_id: impl Into<String>, text: impl Into<String>) -> Self {
128 Self {
129 request_id: request_id.into(),
130 text: text.into(),
131 category: None,
132 timestamp: timestamp(),
133 }
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct CorrectionFeedback {
140 pub request_id: String,
141 pub original_hash: String,
142 pub corrected_hash: String,
143 pub edit_distance: Option<u32>,
144 pub correction_type: Option<String>,
145 pub timestamp: f64,
146}
147impl CorrectionFeedback {
148 pub fn new(
149 request_id: impl Into<String>,
150 original: impl Into<String>,
151 corrected: impl Into<String>,
152 ) -> Self {
153 Self {
154 request_id: request_id.into(),
155 original_hash: original.into(),
156 corrected_hash: corrected.into(),
157 edit_distance: None,
158 correction_type: None,
159 timestamp: timestamp(),
160 }
161 }
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct RegenerateFeedback {
167 pub request_id: String,
168 pub regeneration_count: u32,
169 pub reason: Option<String>,
170 pub timestamp: f64,
171}
172impl RegenerateFeedback {
173 pub fn new(request_id: impl Into<String>) -> Self {
174 Self {
175 request_id: request_id.into(),
176 regeneration_count: 1,
177 reason: None,
178 timestamp: timestamp(),
179 }
180 }
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct StopFeedback {
186 pub request_id: String,
187 pub tokens_generated: Option<u32>,
188 pub reason: Option<String>,
189 pub timestamp: f64,
190}
191impl StopFeedback {
192 pub fn new(request_id: impl Into<String>) -> Self {
193 Self {
194 request_id: request_id.into(),
195 tokens_generated: None,
196 reason: None,
197 timestamp: timestamp(),
198 }
199 }
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204pub enum FeedbackEvent {
205 ChoiceSelection(ChoiceSelectionFeedback),
206 Rating(RatingFeedback),
207 Thumbs(ThumbsFeedback),
208 Text(TextFeedback),
209 Correction(CorrectionFeedback),
210 Regenerate(RegenerateFeedback),
211 Stop(StopFeedback),
212}
213
214impl FeedbackEvent {
215 pub fn request_id(&self) -> &str {
216 match self {
217 FeedbackEvent::ChoiceSelection(f) => &f.request_id,
218 FeedbackEvent::Rating(f) => &f.request_id,
219 FeedbackEvent::Thumbs(f) => &f.request_id,
220 FeedbackEvent::Text(f) => &f.request_id,
221 FeedbackEvent::Correction(f) => &f.request_id,
222 FeedbackEvent::Regenerate(f) => &f.request_id,
223 FeedbackEvent::Stop(f) => &f.request_id,
224 }
225 }
226}
227
228#[async_trait]
230pub trait FeedbackSink: Send + Sync {
231 async fn report(&self, event: FeedbackEvent) -> Result<()>;
232 async fn report_batch(&self, events: Vec<FeedbackEvent>) -> Result<()> {
233 for e in events {
234 self.report(e).await?;
235 }
236 Ok(())
237 }
238 async fn close(&self) -> Result<()> {
239 Ok(())
240 }
241}
242
243pub struct NoopFeedbackSink;
245
246#[async_trait]
247impl FeedbackSink for NoopFeedbackSink {
248 async fn report(&self, _: FeedbackEvent) -> Result<()> {
249 Ok(())
250 }
251}
252
253pub fn noop_sink() -> Arc<dyn FeedbackSink> {
255 Arc::new(NoopFeedbackSink)
256}