cloudiful_docling_convert/
conversion.rs1use 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 pub picture_description_preset: Option<String>,
89}
90
91impl Default for ConversionBehavior {
92 fn default() -> Self {
93 Self {
94 chunker: ChunkerKind::None,
95 chunking: ChunkingOptions::hybrid_defaults(),
96 pipeline: None,
97 picture_description_preset: None,
98 }
99 }
100}
101
102pub fn build_docling_client(config: DoclingRuntimeConfig) -> Result<DoclingClient> {
103 DoclingClient::new(config.into_docling_config())
104}
105
106pub fn build_convert_options(
107 input_kind: InputKind,
108 behavior: &ConversionBehavior,
109) -> Result<ConvertOptions> {
110 let remote_options = RemoteConvertOptions {
111 chunker: behavior.chunker,
112 chunking: behavior.chunking.clone(),
113 pipeline: behavior.pipeline,
114 picture_description_preset: behavior.picture_description_preset.clone(),
115 };
116
117 if matches!(input_kind, InputKind::Pdf) {
118 Ok(ConvertOptions::Pdf(remote_options))
119 } else if input_kind.uses_generic_convert_options() {
120 Ok(ConvertOptions::Generic(remote_options))
121 } else {
122 Ok(ConvertOptions::Text(TextConvertOptions::default()))
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn default_behavior_uses_normal_conversion() {
132 assert_eq!(ConversionBehavior::default().chunker, ChunkerKind::None);
133 }
134
135 #[test]
136 fn generic_inputs_use_native_chunker_options() {
137 let options = build_convert_options(
138 InputKind::Docx,
139 &ConversionBehavior {
140 chunker: ChunkerKind::Hierarchical,
141 ..ConversionBehavior::default()
142 },
143 )
144 .unwrap();
145
146 assert!(matches!(options, ConvertOptions::Generic(_)));
147 }
148
149 #[test]
150 fn text_inputs_remain_local() {
151 let options =
152 build_convert_options(InputKind::Text, &ConversionBehavior::default()).unwrap();
153 assert!(matches!(options, ConvertOptions::Text(_)));
154 }
155
156 #[test]
157 fn runtime_config_keeps_docling_and_openai_keys_separate() {
158 let config =
159 DoclingRuntimeConfig::without_vlm("http://localhost:5001/v1").into_docling_config();
160 assert!(config.api_key.is_none());
161 assert!(config.openai_api_key.is_none());
162 }
163
164 #[test]
165 fn default_behavior_has_no_picture_description_preset() {
166 assert!(
167 ConversionBehavior::default()
168 .picture_description_preset
169 .is_none(),
170 "default ConversionBehavior must not surface a picture_description_preset so legacy custom VLM behaviour stays unchanged"
171 );
172 }
173
174 #[test]
175 fn build_convert_options_propagates_picture_description_preset() {
176 let behavior = ConversionBehavior {
177 picture_description_preset: Some("smolvlm".to_string()),
178 ..ConversionBehavior::default()
179 };
180 let options = build_convert_options(InputKind::Pdf, &behavior).unwrap();
181 match options {
182 ConvertOptions::Pdf(remote) => {
183 assert_eq!(
184 remote.picture_description_preset.as_deref(),
185 Some("smolvlm")
186 );
187 }
188 other => panic!("expected Pdf options, got {other:?}"),
189 }
190
191 let generic = build_convert_options(InputKind::Docx, &behavior).unwrap();
192 match generic {
193 ConvertOptions::Generic(remote) => {
194 assert_eq!(
195 remote.picture_description_preset.as_deref(),
196 Some("smolvlm")
197 );
198 }
199 other => panic!("expected Generic options, got {other:?}"),
200 }
201 }
202}