Skip to main content

ai_lib_rust/
feedback.rs

1//! 核心反馈类型:提供 FeedbackSink trait 和多种反馈事件(始终编译)。
2//!
3//! Core feedback types (always compiled).
4//!
5//! Provides FeedbackSink trait, FeedbackEvent enum, and NoopFeedbackSink for use
6//! by the client and other core modules. The full telemetry module (InMemoryFeedbackSink,
7//! ConsoleFeedbackSink, etc.) is feature-gated.
8
9use 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/// Feedback for multi-candidate selection.
23#[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/// Rating feedback (e.g., 1-5 stars).
57#[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/// Thumbs up/down feedback.
88#[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/// Free-form text feedback.
119#[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/// Correction feedback.
138#[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/// Regeneration feedback.
165#[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/// Stop generation feedback.
184#[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/// Typed feedback events (extensible).
203#[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/// Feedback sink trait.
229#[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
243/// No-op sink (always available).
244pub struct NoopFeedbackSink;
245
246#[async_trait]
247impl FeedbackSink for NoopFeedbackSink {
248    async fn report(&self, _: FeedbackEvent) -> Result<()> {
249        Ok(())
250    }
251}
252
253/// Returns a no-op feedback sink.
254pub fn noop_sink() -> Arc<dyn FeedbackSink> {
255    Arc::new(NoopFeedbackSink)
256}