Skip to main content

ai_lib_rust/telemetry/
mod.rs

1//! 遥测与反馈模块:提供可选的、应用可控的用户反馈收集机制。
2//!
3//! # Telemetry and Feedback Module
4//!
5//! This module provides optional, application-controlled telemetry and feedback
6//! collection capabilities. Privacy is paramount - the runtime MUST NOT force
7//! telemetry collection.
8//!
9//! ## Overview
10//!
11//! The feedback system enables:
12//! - Collection of user preferences (thumbs up/down, ratings)
13//! - Tracking of choice selections in multi-candidate responses
14//! - Recording corrections and regeneration requests
15//! - Custom feedback integration with external systems
16//!
17//! ## Design Principles
18//!
19//! - **Opt-in Only**: No telemetry is collected unless explicitly configured
20//! - **Application-Controlled**: The application decides what to collect and where to send
21//! - **Stable Linkage**: `client_request_id` provides correlation across events
22//! - **Pluggable Sinks**: Implement [`FeedbackSink`] for custom destinations
23//!
24//! ## Key Components
25//!
26//! | Component | Description |
27//! |-----------|-------------|
28//! | [`FeedbackEvent`] | Typed feedback event enum |
29//! | [`FeedbackSink`] | Trait for feedback destinations |
30//! | [`NoopFeedbackSink`] | Default no-op sink (no collection) |
31//! | [`InMemoryFeedbackSink`] | In-memory sink for testing |
32//! | [`ConsoleFeedbackSink`] | Console logging sink for debugging |
33//! | [`CompositeFeedbackSink`] | Multi-destination composite sink |
34//!
35//! ## Feedback Types
36//!
37//! | Type | Description |
38//! |------|-------------|
39//! | [`ThumbsFeedback`] | Simple positive/negative feedback |
40//! | [`RatingFeedback`] | Numeric rating (e.g., 1-5 stars) |
41//! | [`ChoiceSelectionFeedback`] | Multi-candidate selection tracking |
42//! | [`CorrectionFeedback`] | User corrections to model output |
43//! | [`RegenerateFeedback`] | Regeneration request tracking |
44//! | [`TextFeedback`] | Free-form text feedback |
45//!
46//! ## Example
47//!
48//! ```rust
49//! use ai_lib_rust::telemetry::{FeedbackEvent, ThumbsFeedback, set_feedback_sink, InMemoryFeedbackSink};
50//! use std::sync::Arc;
51//!
52//! // Configure feedback collection (opt-in)
53//! let sink = Arc::new(InMemoryFeedbackSink::new(100));
54//! set_feedback_sink(sink.clone());
55//!
56//! // Record feedback
57//! let feedback = ThumbsFeedback::thumbs_up("req-123")
58//!     .with_reason("Helpful response");
59//! // report_feedback(FeedbackEvent::Thumbs(feedback)).await?;
60//! ```
61
62use 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/// Feedback for multi-candidate selection.
71#[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/// Rating feedback (e.g., 1-5 stars).
91#[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/// Thumbs up/down feedback.
102#[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/// Free-form text feedback.
111#[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/// Correction feedback.
118#[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/// Regeneration feedback.
127#[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/// Stop generation feedback.
132#[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/// Typed feedback events (extensible).
137#[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/// Feedback sink trait.
163#[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
170/// No-op sink.
171pub struct NoopFeedbackSink;
172#[async_trait]
173impl FeedbackSink for NoopFeedbackSink { async fn report(&self, _: FeedbackEvent) -> Result<()> { Ok(()) } }
174
175/// In-memory sink for testing.
176pub 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
195/// Console sink for debugging.
196pub 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
202/// Composite sink for multiple destinations.
203pub 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 }