Class: DoclingClient

Inherits:
Object
  • Object
show all
Defined in:
app/services/docling_client.rb

Overview

Thin HTTP client for the docling-serve document-extraction service
(https://github.com/docling-project/docling-serve): file in → Markdown out,
layout-aware (real tables) with OCR for scanned pages built in.

Deployment mirrors PlaywrightRuntime: the app image ships no Python —
docling runs as the docling Kamal accessory in production/staging
(config/deploy*.yml) and as part of the default docker-compose stack in
development (docker compose up -d). The app reaches it over
HTTP via the DOCLING_SERVER_URL env var (e.g. http://heatwave-docling:5001);
when unset, DoclingClient.convert raises NotConfigured.

Spike comparison vs markitdown and integration notes:
doc/integrations/DOCLING_DOCUMENT_EXTRACTION.md.

Examples:

DoclingClient.convert('/tmp/spec_sheet.pdf') # => "| Voltage | 120VAC |…"

Defined Under Namespace

Classes: Error, NotConfigured, Timeout

Constant Summary collapse

SUPPORTED_EXTENSIONS =

File extensions docling-serve can convert (PDF/Office/HTML/CSV/images).
Callers use this to pre-validate before shipping bytes over the network.

%w[.pdf .docx .xlsx .pptx .html .htm .csv .md .adoc
.png .jpg .jpeg .tif .tiff .bmp .webp].freeze
CONVERT_TIMEOUT =

Whole-request timeout in seconds. CPU OCR on a large scanned PDF is the
slow path (~2-30s/page observed in the spike); generous but bounded.

300

Class Method Summary collapse

Class Method Details

.configured?Boolean

Returns whether a docling-serve endpoint is configured.

Returns:

  • (Boolean)

    whether a docling-serve endpoint is configured



54
55
56
# File 'app/services/docling_client.rb', line 54

def configured?
  server_url.present?
end

.convert(path, filename: File.basename(path.to_s)) ⇒ String

Convert a local file to Markdown via docling-serve's synchronous
/v1/convert/file endpoint. OCR runs automatically on scanned/bitmap
content (server default do_ocr=true); tables use the accurate
TableFormer mode (server default). Images become <!-- image -->
placeholders — the output is meant for LLM/text consumption.

Parameters:

  • path (String)

    local file path

  • filename (String) (defaults to: File.basename(path.to_s))

    original filename; its extension tells
    docling-serve the input format (to_file tempfile names keep it)

Returns:

  • (String)

    the extracted Markdown ("" when the document is empty)

Raises:

  • (NotConfigured)

    when DOCLING_SERVER_URL is unset

  • (Error)

    on HTTP or conversion failure



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'app/services/docling_client.rb', line 76

def convert(path, filename: File.basename(path.to_s))
  raise NotConfigured, 'DOCLING_SERVER_URL is not set — boot the docling accessory (prod/staging) or `docker compose up -d` (dev default stack)' unless configured?

  response = HTTP.timeout(CONVERT_TIMEOUT).post(
    "#{server_url}/v1/convert/file",
    form: {
      files:             HTTP::FormData::File.new(path, filename: filename),
      to_formats:        'md',
      image_export_mode: 'placeholder'
    }
  )
  raise Error, "docling-serve returned HTTP #{response.status}: #{response.body.to_s.truncate(300)}" unless response.status.success?

  payload = JSON.parse(response.body.to_s)
  status  = payload['status'].to_s
  raise Error, "docling conversion #{status.presence || 'failed'}: #{Array(payload['errors']).join('; ').truncate(300)}" unless status.in?(%w[success partial_success])

  payload.dig('document', 'md_content').to_s
rescue HTTP::ConnectTimeoutError => e
  # A connection that never established is transient (accessory down/rebooting,
  # network blip) — a plain Error so the worker's retry kicks in. Must precede
  # HTTP::TimeoutError (ConnectTimeoutError < TimeoutError).
  raise Error, "docling-serve connection failed: #{e.message}"
rescue HTTP::TimeoutError => e
  # The conversion blew the 300s sync deadline — permanent (see {Timeout}), not
  # worth a retry. Must precede HTTP::Error (TimeoutError < HTTP::Error).
  raise Timeout, "docling-serve timed out (>#{CONVERT_TIMEOUT}s): #{e.message}"
rescue HTTP::Error => e
  raise Error, "docling-serve request failed: #{e.message}"
rescue JSON::ParserError
  raise Error, 'docling-serve returned invalid JSON'
end

.server_urlString?

Returns base URL of the docling-serve instance.

Returns:

  • (String, nil)

    base URL of the docling-serve instance



49
50
51
# File 'app/services/docling_client.rb', line 49

def server_url
  ENV['DOCLING_SERVER_URL'].presence
end

.supported_file?(filename) ⇒ Boolean

Returns whether docling-serve accepts this file type.

Parameters:

  • filename (String)

    name to test (extension decides)

Returns:

  • (Boolean)

    whether docling-serve accepts this file type



60
61
62
# File 'app/services/docling_client.rb', line 60

def supported_file?(filename)
  File.extname(filename.to_s).downcase.in?(SUPPORTED_EXTENSIONS)
end