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