1use crate::Result;
63use async_trait::async_trait;
64use serde::{Deserialize, Serialize};
65use std::sync::{Arc, RwLock};
66use std::time::{SystemTime, UNIX_EPOCH};
67
68fn timestamp() -> f64 { SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).unwrap_or(0.0) }
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct ChoiceSelectionFeedback {
73 pub request_id: String,
74 pub chosen_index: u32,
75 pub rejected_indices: Option<Vec<u32>>,
76 pub latency_to_select_ms: Option<u64>,
77 pub ui_context: Option<serde_json::Value>,
78 pub candidate_hashes: Option<Vec<String>>,
79 pub timestamp: f64,
80}
81
82impl ChoiceSelectionFeedback {
83 pub fn new(request_id: impl Into<String>, chosen_index: u32) -> Self {
84 Self { request_id: request_id.into(), chosen_index, rejected_indices: None, latency_to_select_ms: None, ui_context: None, candidate_hashes: None, timestamp: timestamp() }
85 }
86 pub fn with_rejected(mut self, indices: Vec<u32>) -> Self { self.rejected_indices = Some(indices); self }
87 pub fn with_latency(mut self, ms: u64) -> Self { self.latency_to_select_ms = Some(ms); self }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct RatingFeedback {
93 pub request_id: String, pub rating: u32, pub max_rating: u32, pub category: Option<String>, pub comment: Option<String>, pub timestamp: f64,
94}
95impl RatingFeedback {
96 pub fn new(request_id: impl Into<String>, rating: u32) -> Self { Self { request_id: request_id.into(), rating, max_rating: 5, category: None, comment: None, timestamp: timestamp() } }
97 pub fn with_max_rating(mut self, m: u32) -> Self { self.max_rating = m; self }
98 pub fn with_comment(mut self, c: impl Into<String>) -> Self { self.comment = Some(c.into()); self }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct ThumbsFeedback { pub request_id: String, pub is_positive: bool, pub reason: Option<String>, pub timestamp: f64 }
104impl ThumbsFeedback {
105 pub fn thumbs_up(request_id: impl Into<String>) -> Self { Self { request_id: request_id.into(), is_positive: true, reason: None, timestamp: timestamp() } }
106 pub fn thumbs_down(request_id: impl Into<String>) -> Self { Self { request_id: request_id.into(), is_positive: false, reason: None, timestamp: timestamp() } }
107 pub fn with_reason(mut self, r: impl Into<String>) -> Self { self.reason = Some(r.into()); self }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct TextFeedback { pub request_id: String, pub text: String, pub category: Option<String>, pub timestamp: f64 }
113impl TextFeedback {
114 pub fn new(request_id: impl Into<String>, text: impl Into<String>) -> Self { Self { request_id: request_id.into(), text: text.into(), category: None, timestamp: timestamp() } }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct CorrectionFeedback { pub request_id: String, pub original_hash: String, pub corrected_hash: String, pub edit_distance: Option<u32>, pub correction_type: Option<String>, pub timestamp: f64 }
120impl CorrectionFeedback {
121 pub fn new(request_id: impl Into<String>, original: impl Into<String>, corrected: impl Into<String>) -> Self {
122 Self { request_id: request_id.into(), original_hash: original.into(), corrected_hash: corrected.into(), edit_distance: None, correction_type: None, timestamp: timestamp() }
123 }
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct RegenerateFeedback { pub request_id: String, pub regeneration_count: u32, pub reason: Option<String>, pub timestamp: f64 }
129impl RegenerateFeedback { pub fn new(request_id: impl Into<String>) -> Self { Self { request_id: request_id.into(), regeneration_count: 1, reason: None, timestamp: timestamp() } } }
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct StopFeedback { pub request_id: String, pub tokens_generated: Option<u32>, pub reason: Option<String>, pub timestamp: f64 }
134impl StopFeedback { pub fn new(request_id: impl Into<String>) -> Self { Self { request_id: request_id.into(), tokens_generated: None, reason: None, timestamp: timestamp() } } }
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub enum FeedbackEvent {
139 ChoiceSelection(ChoiceSelectionFeedback),
140 Rating(RatingFeedback),
141 Thumbs(ThumbsFeedback),
142 Text(TextFeedback),
143 Correction(CorrectionFeedback),
144 Regenerate(RegenerateFeedback),
145 Stop(StopFeedback),
146}
147
148impl FeedbackEvent {
149 pub fn request_id(&self) -> &str {
150 match self {
151 FeedbackEvent::ChoiceSelection(f) => &f.request_id,
152 FeedbackEvent::Rating(f) => &f.request_id,
153 FeedbackEvent::Thumbs(f) => &f.request_id,
154 FeedbackEvent::Text(f) => &f.request_id,
155 FeedbackEvent::Correction(f) => &f.request_id,
156 FeedbackEvent::Regenerate(f) => &f.request_id,
157 FeedbackEvent::Stop(f) => &f.request_id,
158 }
159 }
160}
161
162#[async_trait]
164pub trait FeedbackSink: Send + Sync {
165 async fn report(&self, event: FeedbackEvent) -> Result<()>;
166 async fn report_batch(&self, events: Vec<FeedbackEvent>) -> Result<()> { for e in events { self.report(e).await?; } Ok(()) }
167 async fn close(&self) -> Result<()> { Ok(()) }
168}
169
170pub struct NoopFeedbackSink;
172#[async_trait]
173impl FeedbackSink for NoopFeedbackSink { async fn report(&self, _: FeedbackEvent) -> Result<()> { Ok(()) } }
174
175pub struct InMemoryFeedbackSink { events: Arc<RwLock<Vec<FeedbackEvent>>>, max_events: usize }
177impl InMemoryFeedbackSink {
178 pub fn new(max: usize) -> Self { Self { events: Arc::new(RwLock::new(Vec::new())), max_events: max } }
179 pub fn get_events(&self) -> Vec<FeedbackEvent> { self.events.read().unwrap().clone() }
180 pub fn get_events_by_request(&self, req_id: &str) -> Vec<FeedbackEvent> { self.events.read().unwrap().iter().filter(|e| e.request_id() == req_id).cloned().collect() }
181 pub fn clear(&self) { self.events.write().unwrap().clear(); }
182 pub fn len(&self) -> usize { self.events.read().unwrap().len() }
183 pub fn is_empty(&self) -> bool { self.len() == 0 }
184}
185#[async_trait]
186impl FeedbackSink for InMemoryFeedbackSink {
187 async fn report(&self, event: FeedbackEvent) -> Result<()> {
188 let mut events = self.events.write().unwrap();
189 events.push(event);
190 if events.len() > self.max_events { events.remove(0); }
191 Ok(())
192 }
193}
194
195pub struct ConsoleFeedbackSink { prefix: String }
197impl ConsoleFeedbackSink { pub fn new(prefix: impl Into<String>) -> Self { Self { prefix: prefix.into() } } }
198impl Default for ConsoleFeedbackSink { fn default() -> Self { Self::new("[Feedback]") } }
199#[async_trait]
200impl FeedbackSink for ConsoleFeedbackSink { async fn report(&self, event: FeedbackEvent) -> Result<()> { println!("{} {:?}", self.prefix, event); Ok(()) } }
201
202pub struct CompositeFeedbackSink { sinks: Vec<Arc<dyn FeedbackSink>> }
204impl CompositeFeedbackSink {
205 pub fn new() -> Self { Self { sinks: Vec::new() } }
206 pub fn add_sink(mut self, sink: Arc<dyn FeedbackSink>) -> Self { self.sinks.push(sink); self }
207}
208impl Default for CompositeFeedbackSink { fn default() -> Self { Self::new() } }
209#[async_trait]
210impl FeedbackSink for CompositeFeedbackSink {
211 async fn report(&self, event: FeedbackEvent) -> Result<()> { for s in &self.sinks { let _ = s.report(event.clone()).await; } Ok(()) }
212 async fn close(&self) -> Result<()> { for s in &self.sinks { let _ = s.close().await; } Ok(()) }
213}
214
215pub fn noop_sink() -> Arc<dyn FeedbackSink> { Arc::new(NoopFeedbackSink) }
216
217static GLOBAL_SINK: once_cell::sync::Lazy<RwLock<Arc<dyn FeedbackSink>>> = once_cell::sync::Lazy::new(|| RwLock::new(Arc::new(NoopFeedbackSink)));
218pub fn get_feedback_sink() -> Arc<dyn FeedbackSink> { GLOBAL_SINK.read().unwrap().clone() }
219pub fn set_feedback_sink(sink: Arc<dyn FeedbackSink>) { *GLOBAL_SINK.write().unwrap() = sink; }
220pub async fn report_feedback(event: FeedbackEvent) -> Result<()> { get_feedback_sink().report(event).await }