Skip to main content

ai_lib_rust/telemetry/
mod.rs

1//! Telemetry and feedback (optional, application-controlled).
2//!
3//! The runtime MUST NOT force telemetry collection. Instead it provides:
4//! - a stable `client_request_id` for linkage
5//! - typed feedback events
6//! - an injectable `FeedbackSink` hook (default: no-op)
7
8use crate::Result;
9use async_trait::async_trait;
10use serde::{Deserialize, Serialize};
11use std::sync::{Arc, RwLock};
12use std::time::{SystemTime, UNIX_EPOCH};
13
14fn timestamp() -> f64 { SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64()).unwrap_or(0.0) }
15
16/// Feedback for multi-candidate selection.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ChoiceSelectionFeedback {
19    pub request_id: String,
20    pub chosen_index: u32,
21    pub rejected_indices: Option<Vec<u32>>,
22    pub latency_to_select_ms: Option<u64>,
23    pub ui_context: Option<serde_json::Value>,
24    pub candidate_hashes: Option<Vec<String>>,
25    pub timestamp: f64,
26}
27
28impl ChoiceSelectionFeedback {
29    pub fn new(request_id: impl Into<String>, chosen_index: u32) -> Self {
30        Self { request_id: request_id.into(), chosen_index, rejected_indices: None, latency_to_select_ms: None, ui_context: None, candidate_hashes: None, timestamp: timestamp() }
31    }
32    pub fn with_rejected(mut self, indices: Vec<u32>) -> Self { self.rejected_indices = Some(indices); self }
33    pub fn with_latency(mut self, ms: u64) -> Self { self.latency_to_select_ms = Some(ms); self }
34}
35
36/// Rating feedback (e.g., 1-5 stars).
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct RatingFeedback {
39    pub request_id: String, pub rating: u32, pub max_rating: u32, pub category: Option<String>, pub comment: Option<String>, pub timestamp: f64,
40}
41impl RatingFeedback {
42    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() } }
43    pub fn with_max_rating(mut self, m: u32) -> Self { self.max_rating = m; self }
44    pub fn with_comment(mut self, c: impl Into<String>) -> Self { self.comment = Some(c.into()); self }
45}
46
47/// Thumbs up/down feedback.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct ThumbsFeedback { pub request_id: String, pub is_positive: bool, pub reason: Option<String>, pub timestamp: f64 }
50impl ThumbsFeedback {
51    pub fn thumbs_up(request_id: impl Into<String>) -> Self { Self { request_id: request_id.into(), is_positive: true, reason: None, timestamp: timestamp() } }
52    pub fn thumbs_down(request_id: impl Into<String>) -> Self { Self { request_id: request_id.into(), is_positive: false, reason: None, timestamp: timestamp() } }
53    pub fn with_reason(mut self, r: impl Into<String>) -> Self { self.reason = Some(r.into()); self }
54}
55
56/// Free-form text feedback.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct TextFeedback { pub request_id: String, pub text: String, pub category: Option<String>, pub timestamp: f64 }
59impl TextFeedback {
60    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() } }
61}
62
63/// Correction feedback.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub 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 }
66impl CorrectionFeedback {
67    pub fn new(request_id: impl Into<String>, original: impl Into<String>, corrected: impl Into<String>) -> Self {
68        Self { request_id: request_id.into(), original_hash: original.into(), corrected_hash: corrected.into(), edit_distance: None, correction_type: None, timestamp: timestamp() }
69    }
70}
71
72/// Regeneration feedback.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct RegenerateFeedback { pub request_id: String, pub regeneration_count: u32, pub reason: Option<String>, pub timestamp: f64 }
75impl RegenerateFeedback { pub fn new(request_id: impl Into<String>) -> Self { Self { request_id: request_id.into(), regeneration_count: 1, reason: None, timestamp: timestamp() } } }
76
77/// Stop generation feedback.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct StopFeedback { pub request_id: String, pub tokens_generated: Option<u32>, pub reason: Option<String>, pub timestamp: f64 }
80impl StopFeedback { pub fn new(request_id: impl Into<String>) -> Self { Self { request_id: request_id.into(), tokens_generated: None, reason: None, timestamp: timestamp() } } }
81
82/// Typed feedback events (extensible).
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub enum FeedbackEvent {
85    ChoiceSelection(ChoiceSelectionFeedback),
86    Rating(RatingFeedback),
87    Thumbs(ThumbsFeedback),
88    Text(TextFeedback),
89    Correction(CorrectionFeedback),
90    Regenerate(RegenerateFeedback),
91    Stop(StopFeedback),
92}
93
94impl FeedbackEvent {
95    pub fn request_id(&self) -> &str {
96        match self {
97            FeedbackEvent::ChoiceSelection(f) => &f.request_id,
98            FeedbackEvent::Rating(f) => &f.request_id,
99            FeedbackEvent::Thumbs(f) => &f.request_id,
100            FeedbackEvent::Text(f) => &f.request_id,
101            FeedbackEvent::Correction(f) => &f.request_id,
102            FeedbackEvent::Regenerate(f) => &f.request_id,
103            FeedbackEvent::Stop(f) => &f.request_id,
104        }
105    }
106}
107
108/// Feedback sink trait.
109#[async_trait]
110pub trait FeedbackSink: Send + Sync {
111    async fn report(&self, event: FeedbackEvent) -> Result<()>;
112    async fn report_batch(&self, events: Vec<FeedbackEvent>) -> Result<()> { for e in events { self.report(e).await?; } Ok(()) }
113    async fn close(&self) -> Result<()> { Ok(()) }
114}
115
116/// No-op sink.
117pub struct NoopFeedbackSink;
118#[async_trait]
119impl FeedbackSink for NoopFeedbackSink { async fn report(&self, _: FeedbackEvent) -> Result<()> { Ok(()) } }
120
121/// In-memory sink for testing.
122pub struct InMemoryFeedbackSink { events: Arc<RwLock<Vec<FeedbackEvent>>>, max_events: usize }
123impl InMemoryFeedbackSink {
124    pub fn new(max: usize) -> Self { Self { events: Arc::new(RwLock::new(Vec::new())), max_events: max } }
125    pub fn get_events(&self) -> Vec<FeedbackEvent> { self.events.read().unwrap().clone() }
126    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() }
127    pub fn clear(&self) { self.events.write().unwrap().clear(); }
128    pub fn len(&self) -> usize { self.events.read().unwrap().len() }
129    pub fn is_empty(&self) -> bool { self.len() == 0 }
130}
131#[async_trait]
132impl FeedbackSink for InMemoryFeedbackSink {
133    async fn report(&self, event: FeedbackEvent) -> Result<()> {
134        let mut events = self.events.write().unwrap();
135        events.push(event);
136        if events.len() > self.max_events { events.remove(0); }
137        Ok(())
138    }
139}
140
141/// Console sink for debugging.
142pub struct ConsoleFeedbackSink { prefix: String }
143impl ConsoleFeedbackSink { pub fn new(prefix: impl Into<String>) -> Self { Self { prefix: prefix.into() } } }
144impl Default for ConsoleFeedbackSink { fn default() -> Self { Self::new("[Feedback]") } }
145#[async_trait]
146impl FeedbackSink for ConsoleFeedbackSink { async fn report(&self, event: FeedbackEvent) -> Result<()> { println!("{} {:?}", self.prefix, event); Ok(()) } }
147
148/// Composite sink for multiple destinations.
149pub struct CompositeFeedbackSink { sinks: Vec<Arc<dyn FeedbackSink>> }
150impl CompositeFeedbackSink {
151    pub fn new() -> Self { Self { sinks: Vec::new() } }
152    pub fn add_sink(mut self, sink: Arc<dyn FeedbackSink>) -> Self { self.sinks.push(sink); self }
153}
154impl Default for CompositeFeedbackSink { fn default() -> Self { Self::new() } }
155#[async_trait]
156impl FeedbackSink for CompositeFeedbackSink {
157    async fn report(&self, event: FeedbackEvent) -> Result<()> { for s in &self.sinks { let _ = s.report(event.clone()).await; } Ok(()) }
158    async fn close(&self) -> Result<()> { for s in &self.sinks { let _ = s.close().await; } Ok(()) }
159}
160
161pub fn noop_sink() -> Arc<dyn FeedbackSink> { Arc::new(NoopFeedbackSink) }
162
163static GLOBAL_SINK: once_cell::sync::Lazy<RwLock<Arc<dyn FeedbackSink>>> = once_cell::sync::Lazy::new(|| RwLock::new(Arc::new(NoopFeedbackSink)));
164pub fn get_feedback_sink() -> Arc<dyn FeedbackSink> { GLOBAL_SINK.read().unwrap().clone() }
165pub fn set_feedback_sink(sink: Arc<dyn FeedbackSink>) { *GLOBAL_SINK.write().unwrap() = sink; }
166pub async fn report_feedback(event: FeedbackEvent) -> Result<()> { get_feedback_sink().report(event).await }