kreuzberg 4.2.0 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/README.md +1 -1
  4. data/ext/kreuzberg_rb/native/Cargo.lock +26 -17
  5. data/lib/kreuzberg/cli.rb +16 -6
  6. data/lib/kreuzberg/cli_proxy.rb +3 -1
  7. data/lib/kreuzberg/config.rb +56 -9
  8. data/lib/kreuzberg/djot_content.rb +225 -0
  9. data/lib/kreuzberg/extraction_api.rb +20 -4
  10. data/lib/kreuzberg/result.rb +12 -2
  11. data/lib/kreuzberg/version.rb +1 -1
  12. data/lib/kreuzberg.rb +1 -0
  13. data/sig/kreuzberg.rbs +23 -11
  14. data/spec/binding/batch_spec.rb +6 -5
  15. data/spec/binding/error_recovery_spec.rb +3 -3
  16. data/spec/binding/tables_spec.rb +11 -2
  17. data/spec/unit/config/output_format_spec.rb +18 -18
  18. data/vendor/Cargo.toml +1 -1
  19. data/vendor/kreuzberg/Cargo.toml +1 -1
  20. data/vendor/kreuzberg/README.md +1 -1
  21. data/vendor/kreuzberg/src/api/startup.rs +15 -1
  22. data/vendor/kreuzberg/src/core/config_validation/sections.rs +16 -4
  23. data/vendor/kreuzberg/src/core/extractor/file.rs +1 -2
  24. data/vendor/kreuzberg/src/core/extractor/mod.rs +2 -1
  25. data/vendor/kreuzberg/src/core/io.rs +7 -7
  26. data/vendor/kreuzberg/src/core/mime.rs +4 -4
  27. data/vendor/kreuzberg/src/extraction/pptx/parser.rs +6 -0
  28. data/vendor/kreuzberg/src/plugins/mod.rs +1 -0
  29. data/vendor/kreuzberg/src/plugins/registry/extractor.rs +251 -5
  30. data/vendor/kreuzberg/src/plugins/registry/ocr.rs +150 -2
  31. data/vendor/kreuzberg/src/plugins/registry/processor.rs +213 -5
  32. data/vendor/kreuzberg/src/plugins/registry/validator.rs +220 -4
  33. data/vendor/kreuzberg/src/plugins/startup_validation.rs +385 -0
  34. data/vendor/kreuzberg/tests/config_behavioral.rs +14 -12
  35. data/vendor/kreuzberg/tests/core_integration.rs +2 -4
  36. data/vendor/kreuzberg/tests/mime_detection.rs +3 -2
  37. data/vendor/kreuzberg/tests/pptx_regression_tests.rs +284 -1
  38. data/vendor/kreuzberg-tesseract/Cargo.toml +1 -1
  39. metadata +4 -2
@@ -0,0 +1,385 @@
1
+ //! Startup validation for plugin registries.
2
+ //!
3
+ //! This module provides diagnostics and health checks for plugins
4
+ //! at server startup, helping operators diagnose issues in containerized
5
+ //! environments like Kubernetes.
6
+
7
+ use crate::Result;
8
+ use crate::plugins::registry::{
9
+ get_document_extractor_registry, get_ocr_backend_registry, get_post_processor_registry, get_validator_registry,
10
+ };
11
+
12
+ /// Plugin health status information.
13
+ ///
14
+ /// Contains diagnostic information about registered plugins for each type.
15
+ #[derive(Debug, Clone)]
16
+ pub struct PluginHealthStatus {
17
+ /// Number of registered OCR backends
18
+ pub ocr_backends_count: usize,
19
+ /// Names of registered OCR backends
20
+ pub ocr_backends: Vec<String>,
21
+ /// Number of registered document extractors
22
+ pub extractors_count: usize,
23
+ /// Names of registered document extractors
24
+ pub extractors: Vec<String>,
25
+ /// Number of registered post-processors
26
+ pub post_processors_count: usize,
27
+ /// Names of registered post-processors
28
+ pub post_processors: Vec<String>,
29
+ /// Number of registered validators
30
+ pub validators_count: usize,
31
+ /// Names of registered validators
32
+ pub validators: Vec<String>,
33
+ }
34
+
35
+ impl PluginHealthStatus {
36
+ /// Check plugin health and return status.
37
+ ///
38
+ /// This function reads all plugin registries and collects information
39
+ /// about registered plugins. It logs warnings if critical plugins are missing.
40
+ ///
41
+ /// # Returns
42
+ ///
43
+ /// `PluginHealthStatus` with counts and names of all registered plugins.
44
+ ///
45
+ /// # Example
46
+ ///
47
+ /// ```no_run
48
+ /// use kreuzberg::plugins::startup_validation::PluginHealthStatus;
49
+ ///
50
+ /// #[tokio::main]
51
+ /// async fn main() {
52
+ /// let status = PluginHealthStatus::check();
53
+ /// println!("OCR backends: {:?}", status.ocr_backends);
54
+ /// }
55
+ /// ```
56
+ pub fn check() -> Self {
57
+ let ocr_registry = get_ocr_backend_registry();
58
+ let ocr_backends = ocr_registry.read().map(|r| r.list()).unwrap_or_default();
59
+
60
+ let extractor_registry = get_document_extractor_registry();
61
+ let extractors = extractor_registry.read().map(|r| r.list()).unwrap_or_default();
62
+
63
+ let processor_registry = get_post_processor_registry();
64
+ let post_processors = processor_registry.read().map(|r| r.list()).unwrap_or_default();
65
+
66
+ let validator_registry = get_validator_registry();
67
+ let validators = validator_registry.read().map(|r| r.list()).unwrap_or_default();
68
+
69
+ let ocr_backends_count = ocr_backends.len();
70
+ let extractors_count = extractors.len();
71
+ let post_processors_count = post_processors.len();
72
+ let validators_count = validators.len();
73
+
74
+ PluginHealthStatus {
75
+ ocr_backends_count,
76
+ ocr_backends,
77
+ extractors_count,
78
+ extractors,
79
+ post_processors_count,
80
+ post_processors,
81
+ validators_count,
82
+ validators,
83
+ }
84
+ }
85
+ }
86
+
87
+ /// Validate plugin registries at startup and emit diagnostic logs.
88
+ ///
89
+ /// This function is designed to be called when the API server starts
90
+ /// to help diagnose configuration issues early. It checks:
91
+ ///
92
+ /// - Whether OCR backends are registered (warns if none)
93
+ /// - Whether document extractors are registered (warns if none)
94
+ /// - Environment variables that might affect plugin initialization
95
+ /// - File permission issues in containerized environments
96
+ ///
97
+ /// For Kubernetes deployments, this logs information that helps with
98
+ /// troubleshooting in the container logs.
99
+ ///
100
+ /// # Returns
101
+ ///
102
+ /// - `Ok(PluginHealthStatus)` with diagnostic information
103
+ /// - `Err(KreuzbergError)` if critical issues are detected (currently always succeeds)
104
+ ///
105
+ /// # Example
106
+ ///
107
+ /// ```no_run
108
+ /// use kreuzberg::plugins::startup_validation::validate_plugins_at_startup;
109
+ ///
110
+ /// #[tokio::main]
111
+ /// async fn main() -> kreuzberg::Result<()> {
112
+ /// let status = validate_plugins_at_startup()?;
113
+ /// println!("Plugins ready: {} backends registered", status.ocr_backends_count);
114
+ /// Ok(())
115
+ /// }
116
+ /// ```
117
+ pub fn validate_plugins_at_startup() -> Result<PluginHealthStatus> {
118
+ let status = PluginHealthStatus::check();
119
+
120
+ // Log OCR backend status
121
+ if status.ocr_backends_count == 0 {
122
+ tracing::warn!(
123
+ "No OCR backends registered. OCR functionality will be unavailable. \
124
+ This is normal if OCR is not required. \
125
+ If OCR is needed, check that: \
126
+ 1. The 'ocr' feature is enabled in Cargo.toml \
127
+ 2. TESSDATA_PREFIX environment variable is set (e.g., /usr/share/tesseract-ocr/tessdata) \
128
+ 3. Tessdata files exist and are readable (tessdata/*.traineddata) \
129
+ 4. In containers, mount tessdata volume or install tesseract-ocr package. \
130
+ See https://docs.kreuzberg.dev/guides/docker/ for Kubernetes setup."
131
+ );
132
+ } else {
133
+ tracing::info!(
134
+ "OCR backends registered: [{}]. Ready for OCR processing.",
135
+ status.ocr_backends.join(", ")
136
+ );
137
+ }
138
+
139
+ // Log document extractor status
140
+ if status.extractors_count == 0 {
141
+ tracing::warn!(
142
+ "No document extractors registered. \
143
+ Document extraction will fail. \
144
+ This usually indicates a configuration issue. \
145
+ Ensure extractors are properly registered during initialization."
146
+ );
147
+ } else {
148
+ tracing::info!("Document extractors registered: [{}]", status.extractors.join(", "));
149
+ }
150
+
151
+ // Log post-processor status
152
+ if status.post_processors_count > 0 {
153
+ tracing::info!("Post-processors registered: [{}]", status.post_processors.join(", "));
154
+ }
155
+
156
+ // Log validator status
157
+ if status.validators_count > 0 {
158
+ tracing::info!("Validators registered: [{}]", status.validators.join(", "));
159
+ }
160
+
161
+ // Check for common environment variables
162
+ check_environment_variables();
163
+
164
+ Ok(status)
165
+ }
166
+
167
+ /// Check and log relevant environment variables at startup.
168
+ ///
169
+ /// Logs diagnostics about environment variables that affect plugin behavior,
170
+ /// particularly useful for Kubernetes deployments where configuration
171
+ /// is often done via environment variables.
172
+ fn check_environment_variables() {
173
+ // Check TESSDATA_PREFIX for OCR
174
+ match std::env::var("TESSDATA_PREFIX") {
175
+ Ok(path) => {
176
+ tracing::debug!("TESSDATA_PREFIX={}", path);
177
+ // Verify the path exists
178
+ if let Ok(metadata) = std::fs::metadata(&path) {
179
+ if metadata.is_dir() {
180
+ tracing::debug!(
181
+ "TESSDATA_PREFIX directory exists and is readable. \
182
+ Tesseract should find trained data files."
183
+ );
184
+ } else {
185
+ tracing::warn!(
186
+ "TESSDATA_PREFIX={} exists but is not a directory. \
187
+ Tesseract may fail to initialize.",
188
+ path
189
+ );
190
+ }
191
+ } else {
192
+ tracing::warn!(
193
+ "TESSDATA_PREFIX={} does not exist or is not readable. \
194
+ Tesseract may fail to initialize. \
195
+ Check directory permissions in containerized environments.",
196
+ path
197
+ );
198
+ }
199
+ }
200
+ Err(_) => {
201
+ tracing::debug!("TESSDATA_PREFIX not set. Tesseract will use system default paths.");
202
+ }
203
+ }
204
+
205
+ // Check for common Kubernetes/Docker volume mount points
206
+ if std::path::Path::new("/usr/share/tesseract-ocr/tessdata").exists() {
207
+ tracing::debug!("Found tessdata at system default: /usr/share/tesseract-ocr/tessdata");
208
+ }
209
+
210
+ // Check RUST_LOG for debugging
211
+ if let Ok(log_level) = std::env::var("RUST_LOG") {
212
+ tracing::debug!("RUST_LOG={}", log_level);
213
+ }
214
+ }
215
+
216
+ #[cfg(test)]
217
+ mod tests {
218
+ use super::*;
219
+
220
+ #[test]
221
+ fn test_plugin_health_status_check() {
222
+ let status = PluginHealthStatus::check();
223
+ // Just verify the status can be created (counts are always non-negative)
224
+ let _ = status.ocr_backends_count;
225
+ let _ = status.extractors_count;
226
+ }
227
+
228
+ #[test]
229
+ fn test_validate_plugins_at_startup() {
230
+ // Initialize tracing for tests
231
+ let _ = tracing_subscriber::fmt()
232
+ .with_max_level(tracing::Level::DEBUG)
233
+ .with_test_writer()
234
+ .try_init();
235
+
236
+ let result = validate_plugins_at_startup();
237
+ assert!(result.is_ok());
238
+ let status = result.unwrap();
239
+ // Status created successfully (counts are always non-negative)
240
+ let _ = status.ocr_backends_count;
241
+ }
242
+
243
+ #[test]
244
+ fn test_plugin_health_status_ocr_backends_empty() {
245
+ let status = PluginHealthStatus::check();
246
+ // Status is valid even with no backends
247
+ assert_eq!(status.ocr_backends.len(), status.ocr_backends_count);
248
+ }
249
+
250
+ #[test]
251
+ fn test_plugin_health_status_extractors_empty() {
252
+ let status = PluginHealthStatus::check();
253
+ // Status is valid even with no extractors
254
+ assert_eq!(status.extractors.len(), status.extractors_count);
255
+ }
256
+
257
+ #[test]
258
+ fn test_plugin_health_status_post_processors_empty() {
259
+ let status = PluginHealthStatus::check();
260
+ // Status is valid even with no post-processors
261
+ assert_eq!(status.post_processors.len(), status.post_processors_count);
262
+ }
263
+
264
+ #[test]
265
+ fn test_plugin_health_status_validators_empty() {
266
+ let status = PluginHealthStatus::check();
267
+ // Status is valid even with no validators
268
+ assert_eq!(status.validators.len(), status.validators_count);
269
+ }
270
+
271
+ #[test]
272
+ fn test_validate_plugins_at_startup_returns_status() {
273
+ let _ = tracing_subscriber::fmt()
274
+ .with_max_level(tracing::Level::DEBUG)
275
+ .with_test_writer()
276
+ .try_init();
277
+
278
+ let result = validate_plugins_at_startup();
279
+ assert!(result.is_ok());
280
+
281
+ let status = result.unwrap();
282
+ // Verify all fields are present
283
+ assert_eq!(status.ocr_backends.len(), status.ocr_backends_count);
284
+ assert_eq!(status.extractors.len(), status.extractors_count);
285
+ assert_eq!(status.post_processors.len(), status.post_processors_count);
286
+ assert_eq!(status.validators.len(), status.validators_count);
287
+ }
288
+
289
+ #[test]
290
+ fn test_plugin_health_status_check_consistency() {
291
+ let status1 = PluginHealthStatus::check();
292
+ let status2 = PluginHealthStatus::check();
293
+
294
+ // Counts should be consistent between calls
295
+ assert_eq!(status1.ocr_backends_count, status2.ocr_backends_count);
296
+ assert_eq!(status1.extractors_count, status2.extractors_count);
297
+ assert_eq!(status1.post_processors_count, status2.post_processors_count);
298
+ assert_eq!(status1.validators_count, status2.validators_count);
299
+ }
300
+
301
+ #[test]
302
+ fn test_validate_plugins_at_startup_with_logging() {
303
+ // Initialize tracing with test writer
304
+ let _ = tracing_subscriber::fmt()
305
+ .with_max_level(tracing::Level::INFO)
306
+ .with_test_writer()
307
+ .try_init();
308
+
309
+ let result = validate_plugins_at_startup();
310
+ assert!(result.is_ok());
311
+
312
+ // Verify status is returned
313
+ let status = result.unwrap();
314
+ assert!(status.ocr_backends_count > 0);
315
+ }
316
+
317
+ #[test]
318
+ fn test_plugin_health_status_all_counts_valid() {
319
+ let status = PluginHealthStatus::check();
320
+
321
+ // All counts should be valid and consistent with vectors
322
+ assert_eq!(status.ocr_backends.len(), status.ocr_backends_count);
323
+ assert_eq!(status.extractors.len(), status.extractors_count);
324
+ assert_eq!(status.post_processors.len(), status.post_processors_count);
325
+ assert_eq!(status.validators.len(), status.validators_count);
326
+ }
327
+
328
+ #[test]
329
+ fn test_plugin_health_status_vec_sizes_match_counts() {
330
+ let status = PluginHealthStatus::check();
331
+
332
+ // Vector sizes should match their counts
333
+ assert_eq!(status.ocr_backends.len(), status.ocr_backends_count);
334
+ assert_eq!(status.extractors.len(), status.extractors_count);
335
+ assert_eq!(status.post_processors.len(), status.post_processors_count);
336
+ assert_eq!(status.validators.len(), status.validators_count);
337
+ }
338
+
339
+ #[test]
340
+ fn test_validate_plugins_at_startup_logs_warnings_and_info() {
341
+ let _ = tracing_subscriber::fmt()
342
+ .with_max_level(tracing::Level::DEBUG)
343
+ .with_test_writer()
344
+ .try_init();
345
+
346
+ // Call validation which should log warnings if no extractors
347
+ let result = validate_plugins_at_startup();
348
+ assert!(result.is_ok());
349
+
350
+ let status = result.unwrap();
351
+ assert_eq!(status.ocr_backends.len(), status.ocr_backends_count);
352
+ }
353
+
354
+ #[test]
355
+ fn test_check_environment_variables_with_rust_log() {
356
+ let _ = tracing_subscriber::fmt()
357
+ .with_max_level(tracing::Level::DEBUG)
358
+ .with_test_writer()
359
+ .try_init();
360
+
361
+ // This test just verifies that check_environment_variables doesn't panic
362
+ let result = validate_plugins_at_startup();
363
+ assert!(result.is_ok());
364
+ }
365
+
366
+ #[test]
367
+ fn test_plugin_health_status_clone() {
368
+ let status1 = PluginHealthStatus::check();
369
+ let status2 = status1.clone();
370
+
371
+ // Cloned status should be equal to original
372
+ assert_eq!(status1.ocr_backends_count, status2.ocr_backends_count);
373
+ assert_eq!(status1.extractors_count, status2.extractors_count);
374
+ assert_eq!(status1.post_processors_count, status2.post_processors_count);
375
+ assert_eq!(status1.validators_count, status2.validators_count);
376
+ }
377
+
378
+ #[test]
379
+ fn test_plugin_health_status_debug_format() {
380
+ let status = PluginHealthStatus::check();
381
+ let debug_str = format!("{:?}", status);
382
+ assert!(!debug_str.is_empty());
383
+ assert!(debug_str.contains("ocr_backends_count"));
384
+ }
385
+ }
@@ -46,6 +46,7 @@ async fn test_output_format_plain_produces_plain() {
46
46
 
47
47
  /// Test output_format Markdown produces markdown formatting
48
48
  #[tokio::test]
49
+ #[cfg(feature = "html")]
49
50
  async fn test_output_format_markdown_produces_markdown() {
50
51
  let html = b"<h1>Title</h1><p>Paragraph with <strong>bold</strong> text.</p>";
51
52
 
@@ -195,20 +196,20 @@ async fn test_chunking_overlap_creates_overlap() {
195
196
  .await
196
197
  .expect("Should extract successfully");
197
198
 
198
- if let Some(chunks) = result.chunks {
199
- if chunks.len() >= 2 {
200
- // Check if adjacent chunks have overlapping text
201
- let chunk1_end = &chunks[0].content[chunks[0].content.len().saturating_sub(15)..];
202
- let chunk2_start = &chunks[1].content[..chunks[1].content.len().min(15)];
199
+ if let Some(chunks) = result.chunks
200
+ && chunks.len() >= 2
201
+ {
202
+ // Check if adjacent chunks have overlapping text
203
+ let chunk1_end = &chunks[0].content[chunks[0].content.len().saturating_sub(15)..];
204
+ let chunk2_start = &chunks[1].content[..chunks[1].content.len().min(15)];
203
205
 
204
- // There should be some overlap in the text
205
- let overlap_found = chunk1_end.chars().any(|c| c != ' ') && chunk2_start.chars().any(|c| c != ' ');
206
+ // There should be some overlap in the text
207
+ let overlap_found = chunk1_end.chars().any(|c| c != ' ') && chunk2_start.chars().any(|c| c != ' ');
206
208
 
207
- assert!(
208
- overlap_found,
209
- "Adjacent chunks should have overlapping non-whitespace text"
210
- );
211
- }
209
+ assert!(
210
+ overlap_found,
211
+ "Adjacent chunks should have overlapping non-whitespace text"
212
+ );
212
213
  }
213
214
  }
214
215
 
@@ -318,6 +319,7 @@ async fn test_quality_processing_disabled_no_score() {
318
319
 
319
320
  /// Test output_format combinations with result_format
320
321
  #[tokio::test]
322
+ #[cfg(feature = "html")]
321
323
  async fn test_output_format_with_element_based() {
322
324
  let html = b"<p>First paragraph</p><p>Second paragraph</p>";
323
325
 
@@ -408,10 +408,8 @@ async fn test_nonexistent_file_error() {
408
408
  let result = extract_file("/nonexistent/file.txt", None, &config).await;
409
409
 
410
410
  assert!(result.is_err());
411
- assert!(matches!(
412
- result.unwrap_err(),
413
- kreuzberg::KreuzbergError::Validation { .. }
414
- ));
411
+ // File validation returns Io error for missing files (NotFound)
412
+ assert!(matches!(result.unwrap_err(), kreuzberg::KreuzbergError::Io(_)));
415
413
  }
416
414
 
417
415
  /// Test error handling for unsupported MIME types.
@@ -327,9 +327,10 @@ async fn test_mime_detection_nonexistent_file() {
327
327
  assert!(result.is_err(), "Should fail for nonexistent file");
328
328
 
329
329
  let error = result.unwrap_err();
330
+ // File existence check returns Io error (NotFound), not Validation error
330
331
  assert!(
331
- matches!(error, kreuzberg::KreuzbergError::Validation { .. }),
332
- "Should return Validation error for nonexistent file"
332
+ matches!(error, kreuzberg::KreuzbergError::Io(_)),
333
+ "Should return Io error for nonexistent file"
333
334
  );
334
335
  }
335
336