1use std::sync::Arc;
7
8#[derive(Debug, Clone)]
10pub enum ProgressEvent {
11 ProcessingStarted {
13 text_length: usize,
14 model: String,
15 provider: String,
16 },
17 ChunkingStarted {
19 total_chars: usize,
20 chunk_count: usize,
21 strategy: String,
22 },
23 BatchProgress {
25 batch_number: usize,
26 total_batches: usize,
27 chunks_processed: usize,
28 total_chunks: usize,
29 },
30 ModelCall {
32 provider: String,
33 model: String,
34 input_length: usize,
35 },
36 ModelResponse {
38 success: bool,
39 output_length: Option<usize>,
40 },
41 ValidationStarted {
43 raw_output_length: usize,
44 },
45 ValidationCompleted {
47 extractions_found: usize,
48 aligned_count: usize,
49 errors: usize,
50 warnings: usize,
51 },
52 AggregationStarted {
54 chunk_count: usize,
55 },
56 ProcessingCompleted {
58 total_extractions: usize,
59 processing_time_ms: u64,
60 },
61 RetryAttempt {
63 operation: String,
64 attempt: usize,
65 max_attempts: usize,
66 delay_seconds: u64,
67 },
68 Error {
70 operation: String,
71 error: String,
72 },
73 Debug {
75 operation: String,
76 details: String,
77 },
78}
79
80pub trait ProgressHandler: Send + Sync {
82 fn handle_progress(&self, event: ProgressEvent);
84}
85
86pub struct ConsoleProgressHandler {
88 pub show_progress: bool,
90 pub show_debug: bool,
92 pub use_styling: bool,
94}
95
96impl ConsoleProgressHandler {
97 pub fn new() -> Self {
99 Self {
100 show_progress: true,
101 show_debug: false,
102 use_styling: true,
103 }
104 }
105
106 pub fn quiet() -> Self {
108 Self {
109 show_progress: false,
110 show_debug: false,
111 use_styling: true,
112 }
113 }
114
115 pub fn verbose() -> Self {
117 Self {
118 show_progress: true,
119 show_debug: true,
120 use_styling: true,
121 }
122 }
123
124 pub fn machine_readable() -> Self {
126 Self {
127 show_progress: true,
128 show_debug: false,
129 use_styling: false,
130 }
131 }
132
133 fn format_message(&self, emoji: &str, message: &str) -> String {
134 if self.use_styling {
135 format!("{} {}", emoji, message)
136 } else {
137 format!("[{}] {}",
138 match emoji {
139 "🤖" => "MODEL",
140 "📄" => "CHUNK",
141 "🔄" => "PROGRESS",
142 "✅" => "SUCCESS",
143 "❌" => "ERROR",
144 "⏳" => "WAIT",
145 "🎯" => "COMPLETE",
146 "🔧" => "DEBUG",
147 "📥" => "RESPONSE",
148 "📡" => "REQUEST",
149 "🎉" => "SUCCESS",
150 _ => "INFO",
151 },
152 message
153 )
154 }
155 }
156}
157
158impl Default for ConsoleProgressHandler {
159 fn default() -> Self {
160 Self::new()
161 }
162}
163
164impl ProgressHandler for ConsoleProgressHandler {
165 fn handle_progress(&self, event: ProgressEvent) {
166 match event {
167 ProgressEvent::ProcessingStarted { text_length, model, provider } => {
168 if self.show_progress {
169 let msg = format!("Calling {} model: {} ({} chars input)", provider, model, text_length);
170 println!("{}", self.format_message("🤖", &msg));
171 }
172 }
173 ProgressEvent::ChunkingStarted { total_chars, chunk_count, strategy } => {
174 if self.show_progress {
175 let msg = format!("Processing document with {} {} chunks ({} chars total)",
176 chunk_count, strategy, total_chars);
177 println!("{}", self.format_message("📄", &msg));
178 }
179 }
180 ProgressEvent::BatchProgress { batch_number, total_batches: _, chunks_processed, total_chunks } => {
181 if self.show_progress {
182 let msg = format!("Processing batch {} ({}/{} chunks processed)",
183 batch_number, chunks_processed, total_chunks);
184 println!("{}", self.format_message("🔄", &msg));
185 }
186 }
187 ProgressEvent::ModelCall { provider, model: _, input_length } => {
188 if self.show_progress {
189 let msg = format!("Starting {} API call with retry logic for input size {}", provider, input_length);
190 println!("{}", self.format_message("🔄", &msg));
191 }
192 }
193 ProgressEvent::ModelResponse { success, output_length: _ } => {
194 if self.show_progress {
195 let msg = if success {
196 "Received response from language model".to_string()
197 } else {
198 "Failed to receive response from language model".to_string()
199 };
200 println!("{}", self.format_message("📥", &msg));
201 }
202 }
203 ProgressEvent::AggregationStarted { chunk_count } => {
204 if self.show_progress {
205 let msg = format!("Aggregating results from {} chunks...", chunk_count);
206 println!("{}", self.format_message("🔄", &msg));
207 }
208 }
209 ProgressEvent::ProcessingCompleted { total_extractions, processing_time_ms: _ } => {
210 if self.show_progress {
211 let msg = format!("Extraction complete! Found {} total extractions", total_extractions);
212 println!("{}", self.format_message("🎯", &msg));
213 }
214 }
215 ProgressEvent::RetryAttempt { operation, attempt, max_attempts, delay_seconds } => {
216 if self.show_progress {
217 println!("{}", self.format_message("❌",
218 &format!("{} failed (attempt {}/{})", operation, attempt, max_attempts)));
219 println!("{}", self.format_message("⏳",
220 &format!("Retrying in {} seconds...", delay_seconds)));
221 }
222 }
223 ProgressEvent::Error { operation, error } => {
224 println!("{}", self.format_message("❌", &format!("{}: {}", operation, error)));
226 }
227 ProgressEvent::Debug { operation, details } => {
228 if self.show_debug {
229 println!("{}", self.format_message("🔧", &format!("DEBUG: {}: {}", operation, details)));
230 }
231 }
232 ProgressEvent::ValidationStarted { raw_output_length: _ } => {
233 }
235 ProgressEvent::ValidationCompleted { extractions_found, aligned_count, errors, warnings } => {
236 if self.show_debug {
237 let msg = format!("Validation result: {} extractions ({} aligned), {} errors, {} warnings",
238 extractions_found, aligned_count, errors, warnings);
239 println!("{}", self.format_message("🔧", &format!("DEBUG: {}", msg)));
240 }
241 }
242 }
243 }
244}
245
246pub struct SilentProgressHandler;
248
249impl ProgressHandler for SilentProgressHandler {
250 fn handle_progress(&self, _event: ProgressEvent) {
251 }
253}
254
255pub struct LogProgressHandler;
257
258impl ProgressHandler for LogProgressHandler {
259 fn handle_progress(&self, event: ProgressEvent) {
260 match event {
261 ProgressEvent::ProcessingStarted { text_length, model, provider } => {
262 log::info!("Starting extraction with {} model {} ({} chars)", provider, model, text_length);
263 }
264 ProgressEvent::ChunkingStarted { total_chars, chunk_count, strategy } => {
265 log::info!("Chunking document: {} {} chunks ({} chars)", chunk_count, strategy, total_chars);
266 }
267 ProgressEvent::BatchProgress { batch_number, total_batches: _, chunks_processed, total_chunks } => {
268 log::debug!("Processing batch {}: {}/{} chunks", batch_number, chunks_processed, total_chunks);
269 }
270 ProgressEvent::ModelCall { provider, model, input_length } => {
271 log::debug!("Calling {} model {} with {} chars input", provider, model, input_length);
272 }
273 ProgressEvent::ModelResponse { success, output_length } => {
274 if success {
275 log::debug!("Received response: {} chars", output_length.unwrap_or(0));
276 } else {
277 log::warn!("Failed to receive model response");
278 }
279 }
280 ProgressEvent::ValidationCompleted { extractions_found, aligned_count, errors, warnings } => {
281 log::debug!("Validation: {} extractions ({} aligned), {} errors, {} warnings",
282 extractions_found, aligned_count, errors, warnings);
283 }
284 ProgressEvent::AggregationStarted { chunk_count } => {
285 log::debug!("Aggregating {} chunks", chunk_count);
286 }
287 ProgressEvent::ProcessingCompleted { total_extractions, processing_time_ms } => {
288 log::info!("Extraction completed: {} extractions in {}ms", total_extractions, processing_time_ms);
289 }
290 ProgressEvent::RetryAttempt { operation, attempt, max_attempts, delay_seconds } => {
291 log::warn!("Retry {}/{} for {}, waiting {}s", attempt, max_attempts, operation, delay_seconds);
292 }
293 ProgressEvent::Error { operation, error } => {
294 log::error!("{}: {}", operation, error);
295 }
296 ProgressEvent::Debug { operation, details } => {
297 log::debug!("{}: {}", operation, details);
298 }
299 ProgressEvent::ValidationStarted { .. } => {
300 log::trace!("Starting validation");
301 }
302 }
303 }
304}
305
306static mut PROGRESS_HANDLER: Option<Arc<dyn ProgressHandler>> = None;
308static INIT: std::sync::Once = std::sync::Once::new();
309
310pub fn init_progress_handler(handler: Arc<dyn ProgressHandler>) {
312 unsafe {
313 PROGRESS_HANDLER = Some(handler);
314 }
315}
316
317fn get_progress_handler() -> Arc<dyn ProgressHandler> {
319 unsafe {
320 PROGRESS_HANDLER.clone().unwrap_or_else(|| {
321 INIT.call_once(|| {
322 PROGRESS_HANDLER = Some(Arc::new(ConsoleProgressHandler::new()));
323 });
324 PROGRESS_HANDLER.clone().unwrap()
325 })
326 }
327}
328
329pub fn report_progress(event: ProgressEvent) {
331 let handler = get_progress_handler();
332 handler.handle_progress(event);
333}
334
335#[macro_export]
337macro_rules! progress_info {
338 ($($arg:tt)*) => {
339 $crate::logging::report_progress($crate::logging::ProgressEvent::Debug {
340 operation: "info".to_string(),
341 details: format!($($arg)*),
342 });
343 };
344}
345
346#[macro_export]
347macro_rules! progress_debug {
348 ($operation:expr, $($arg:tt)*) => {
349 $crate::logging::report_progress($crate::logging::ProgressEvent::Debug {
350 operation: $operation.to_string(),
351 details: format!($($arg)*),
352 });
353 };
354}
355
356#[macro_export]
357macro_rules! progress_error {
358 ($operation:expr, $($arg:tt)*) => {
359 $crate::logging::report_progress($crate::logging::ProgressEvent::Error {
360 operation: $operation.to_string(),
361 error: format!($($arg)*),
362 });
363 };
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 #[test]
371 fn test_console_handler_formatting() {
372 let handler = ConsoleProgressHandler::new();
373 let message = handler.format_message("🤖", "Test message");
374 assert!(message.contains("🤖"));
375 assert!(message.contains("Test message"));
376
377 let machine_handler = ConsoleProgressHandler::machine_readable();
378 let machine_message = machine_handler.format_message("🤖", "Test message");
379 assert!(machine_message.contains("[MODEL]"));
380 assert!(machine_message.contains("Test message"));
381 }
382
383 #[test]
384 fn test_progress_events() {
385 let handler = ConsoleProgressHandler::quiet();
386
387 handler.handle_progress(ProgressEvent::ProcessingStarted {
389 text_length: 1000,
390 model: "test-model".to_string(),
391 provider: "test-provider".to_string(),
392 });
393 }
394}