Skip to main content

cloudiful_docling_convert/
conversion.rs

1use std::time::Duration;
2
3use crate::api::{DoclingClient, DoclingConfig};
4use crate::document::{
5    ChunkerKind, ChunkingOptions, ConvertOptions, InputKind, PipelineKind, RemoteConvertOptions,
6    TextConvertOptions,
7};
8use crate::error::Result;
9
10#[derive(Debug, Clone)]
11pub struct DoclingRuntimeConfig {
12    pub docling_base_url: String,
13    pub openai_base_url: String,
14    pub vlm_pipeline_model: String,
15    pub picture_description_model: String,
16    pub code_formula_model: String,
17    pub api_key: Option<String>,
18    pub tenant_id: Option<String>,
19    pub openai_api_key: Option<String>,
20    pub request_timeout: Option<Duration>,
21}
22
23impl DoclingRuntimeConfig {
24    pub fn without_vlm(docling_base_url: impl Into<String>) -> Self {
25        Self {
26            docling_base_url: docling_base_url.into(),
27            openai_base_url: String::new(),
28            vlm_pipeline_model: String::new(),
29            picture_description_model: String::new(),
30            code_formula_model: String::new(),
31            api_key: None,
32            tenant_id: None,
33            openai_api_key: None,
34            request_timeout: None,
35        }
36    }
37
38    pub fn from_env(docling_base_url: impl Into<String>) -> Self {
39        let mut config = Self::without_vlm(docling_base_url);
40        config.api_key = std::env::var("DOCLING_API_KEY").ok();
41        config.tenant_id = std::env::var("DOCLING_TENANT_ID").ok();
42        config.openai_api_key = std::env::var("OPENAI_API_KEY").ok();
43        config.request_timeout = std::env::var("DOCLING_HTTP_TIMEOUT_SECS")
44            .ok()
45            .and_then(|value| value.parse::<u64>().ok())
46            .map(Duration::from_secs);
47        config
48    }
49
50    pub fn into_docling_config(self) -> DoclingConfig {
51        DoclingConfig {
52            base_url: self.docling_base_url,
53            openai_base_url: self.openai_base_url,
54            vlm_pipeline_model: self.vlm_pipeline_model,
55            picture_description_model: self.picture_description_model,
56            code_formula_model: self.code_formula_model,
57            api_key: self
58                .api_key
59                .or_else(|| std::env::var("DOCLING_API_KEY").ok()),
60            openai_api_key: self
61                .openai_api_key
62                .or_else(|| std::env::var("OPENAI_API_KEY").ok()),
63            tenant_id: self
64                .tenant_id
65                .or_else(|| std::env::var("DOCLING_TENANT_ID").ok()),
66            request_timeout: self.request_timeout,
67        }
68    }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct ConversionBehavior {
73    pub chunker: ChunkerKind,
74    pub chunking: ChunkingOptions,
75    pub pipeline: Option<PipelineKind>,
76}
77
78impl Default for ConversionBehavior {
79    fn default() -> Self {
80        Self {
81            chunker: ChunkerKind::None,
82            chunking: ChunkingOptions::hybrid_defaults(),
83            pipeline: None,
84        }
85    }
86}
87
88pub fn build_docling_client(config: DoclingRuntimeConfig) -> Result<DoclingClient> {
89    DoclingClient::new(config.into_docling_config())
90}
91
92pub fn build_convert_options(
93    input_kind: InputKind,
94    behavior: &ConversionBehavior,
95) -> Result<ConvertOptions> {
96    let remote_options = RemoteConvertOptions {
97        chunker: behavior.chunker,
98        chunking: behavior.chunking.clone(),
99        pipeline: behavior.pipeline,
100    };
101
102    if matches!(input_kind, InputKind::Pdf) {
103        Ok(ConvertOptions::Pdf(remote_options))
104    } else if input_kind.uses_generic_convert_options() {
105        Ok(ConvertOptions::Generic(remote_options))
106    } else {
107        Ok(ConvertOptions::Text(TextConvertOptions::default()))
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn default_behavior_uses_normal_conversion() {
117        assert_eq!(ConversionBehavior::default().chunker, ChunkerKind::None);
118    }
119
120    #[test]
121    fn generic_inputs_use_native_chunker_options() {
122        let options = build_convert_options(
123            InputKind::Docx,
124            &ConversionBehavior {
125                chunker: ChunkerKind::Hierarchical,
126                ..ConversionBehavior::default()
127            },
128        )
129        .unwrap();
130
131        assert!(matches!(options, ConvertOptions::Generic(_)));
132    }
133
134    #[test]
135    fn text_inputs_remain_local() {
136        let options =
137            build_convert_options(InputKind::Text, &ConversionBehavior::default()).unwrap();
138        assert!(matches!(options, ConvertOptions::Text(_)));
139    }
140
141    #[test]
142    fn runtime_config_keeps_docling_and_openai_keys_separate() {
143        let config =
144            DoclingRuntimeConfig::without_vlm("http://localhost:5001/v1").into_docling_config();
145        assert!(config.api_key.is_none());
146        assert!(config.openai_api_key.is_none());
147    }
148}