Skip to main content

7.12 documents: docling-serve client

// crates/documents/src/lib.rs
use async_trait::async_trait;
use bo_core::{domain::ParsedDocument, ports::DocumentParser, CoreError, Result};

/// `options` comes from settings.documents.parsers.<name>.options in config/{env}.yaml.
pub struct DoclingClient { http: reqwest::Client, base: String, api_key: Option<String>, options: serde_json::Value }
impl DoclingClient {
pub fn new(base: String, api_key: Option<String>, options: serde_json::Value) -> Self {
Self { http: reqwest::Client::new(), base, api_key, options }
}
}

#[async_trait]
impl DocumentParser for DoclingClient {
async fn parse(&self, filename: &str, bytes: Vec<u8>) -> Result<ParsedDocument> {
let part = reqwest::multipart::Part::bytes(bytes).file_name(filename.to_string());
let mut form = reqwest::multipart::Form::new().part("files", part)
.text("to_formats", "md").text("to_formats", "json");
if let Some(o) = self.options.as_object() { // do_ocr, table_mode, do_picture_classification … (VERIFY option names)
for (k, v) in o { form = form.text(k.clone(), v.as_str().map(str::to_string).unwrap_or_else(|| v.to_string())); }
}
let mut req = self.http.post(format!("{}/v1/convert/file", self.base)).multipart(form);
if let Some(k) = self.api_key.as_deref().filter(|k| !k.is_empty()) { req = req.header("X-Api-Key", k); }
let v: serde_json::Value = req.send().await.map_err(|e| CoreError::Retryable(e.to_string()))?
.error_for_status().map_err(|e| CoreError::Document(e.to_string()))?
.json().await.map_err(|e| CoreError::Document(e.to_string()))?;
// TODO(impl): large files -> the async convert endpoint, then poll for the result
Ok(ParsedDocument {
markdown: v["document"]["md_content"].as_str().unwrap_or_default().to_string(),
json: v["document"]["json_content"].clone(),
pages: v["document"]["json_content"]["pages"].as_object().map_or(0, |m| m.len() as u32),
})
}
}