Skip to content

✨ add support for workflow polling #185

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/code_samples/workflow_execution.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# gem install mindee
#

require_relative 'mindee'
require 'mindee'

workflow_id = 'workflow-id'

Expand Down
36 changes: 36 additions & 0 deletions docs/code_samples/workflow_polling.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#
# Install the Ruby client library by running:
# gem install mindee
#

require 'mindee'

workflow_id = 'workflow-id'

# Init a new client
mindee_client = Mindee::Client.new

# Load a file from disk
input_source = mindee_client.source_from_path('path/to/my/file.ext')

# Initialize a custom endpoint for this product
custom_endpoint = mindee_client.create_endpoint(
account_name: 'my-account',
endpoint_name: 'my-endpoint',
version: 'my-version'
)

# Parse the file
result = mindee_client.parse(
input_source,
Mindee::Product::Universal::Universal,
endpoint: custom_endpoint,
options: {
rag: true,
workflow_id: workflow_id
}
)

# Print a full summary of the parsed data in RST format
puts result.document

26 changes: 13 additions & 13 deletions lib/mindee/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ def initialize(params: {})
# @!attribute delay_sec [Numeric] Delay between polling attempts. Defaults to 1.5.
# @!attribute max_retries [Integer] Maximum number of retries. Defaults to 80.
class ParseOptions
attr_accessor :all_words, :full_text, :close_file, :page_options, :cropper,
:initial_delay_sec, :delay_sec, :max_retries
attr_accessor :all_words, :full_text, :close_file, :page_options, :cropper, :rag,
:workflow_id, :initial_delay_sec, :delay_sec, :max_retries

def initialize(params: {})
params = params.transform_keys(&:to_sym)
Expand All @@ -66,6 +66,8 @@ def initialize(params: {})
raw_page_options = PageOptions.new(params: raw_page_options) unless raw_page_options.is_a?(PageOptions)
@page_options = raw_page_options
@cropper = params.fetch(:cropper, false)
@rag = params.fetch(:rag, false)
@workflow_id = params.fetch(:workflow_id, nil)
@initial_delay_sec = params.fetch(:initial_delay_sec, 2)
@delay_sec = params.fetch(:delay_sec, 1.5)
@max_retries = params.fetch(:max_retries, 80)
Expand Down Expand Up @@ -176,13 +178,10 @@ def parse_sync(input_source, product_class, endpoint, options)

prediction, raw_http = endpoint.predict(
input_source,
options.all_words,
options.full_text,
options.close_file,
options.cropper
options
)

Mindee::Parsing::Common::ApiResponse.new(product_class, prediction, raw_http)
Mindee::Parsing::Common::ApiResponse.new(product_class, prediction, raw_http.to_s)
end

# Enqueue a document for async parsing
Expand All @@ -207,6 +206,8 @@ def parse_sync(input_source, product_class, endpoint, options)
# - `:on_min_pages` [Integer] Apply the operation only if the document has at least this many pages.
# * `:cropper` [bool] Whether to include cropper results for each page.
# This performs a cropping operation on the server and will increase response time.
# * `:rag` [bool] Whether to enable Retrieval-Augmented Generation. Only works if a Workflow ID is provided.
# * `:workflow_id` [String, nil] ID of the workflow to use.
# @param endpoint [Mindee::HTTP::Endpoint] Endpoint of the API.
# @return [Mindee::Parsing::Common::ApiResponse]
def enqueue(input_source, product_class, endpoint: nil, options: {})
Expand All @@ -216,12 +217,9 @@ def enqueue(input_source, product_class, endpoint: nil, options: {})

prediction, raw_http = endpoint.predict_async(
input_source,
opts.all_words,
opts.full_text,
opts.close_file,
opts.cropper
opts
)
Mindee::Parsing::Common::ApiResponse.new(product_class, prediction, raw_http)
Mindee::Parsing::Common::ApiResponse.new(product_class, prediction, raw_http.to_json)
end

# Parses a queued document
Expand All @@ -236,7 +234,7 @@ def parse_queued(job_id, product_class, endpoint: nil)
endpoint = initialize_endpoint(product_class) if endpoint.nil?
logger.debug("Fetching queued document as '#{endpoint.url_root}'")
prediction, raw_http = endpoint.parse_async(job_id)
Mindee::Parsing::Common::ApiResponse.new(product_class, prediction, raw_http)
Mindee::Parsing::Common::ApiResponse.new(product_class, prediction, raw_http.to_json)
end

# Enqueue a document for async parsing and automatically try to retrieve it
Expand All @@ -261,6 +259,8 @@ def parse_queued(job_id, product_class, endpoint: nil)
# - `:on_min_pages` [Integer] Apply the operation only if the document has at least this many pages.
# * `:cropper` [bool, nil] Whether to include cropper results for each page.
# This performs a cropping operation on the server and will increase response time.
# * `:rag` [bool] Whether to enable Retrieval-Augmented Generation. Only works if a Workflow ID is provided.
# * `:workflow_id` [String, nil] ID of the workflow to use.
# * `:initial_delay_sec` [Numeric] Initial delay before polling. Defaults to 2.
# * `:delay_sec` [Numeric] Delay between polling attempts. Defaults to 1.5.
# * `:max_retries` [Integer] Maximum number of retries. Defaults to 80.
Expand Down
84 changes: 37 additions & 47 deletions lib/mindee/http/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class Endpoint
attr_reader :request_timeout
# @return [String]
attr_reader :url_root
# @return [String]
attr_reader :base_url

def initialize(owner, url_name, version, api_key: '')
@owner = owner
Expand All @@ -44,25 +46,19 @@ def initialize(owner, url_name, version, api_key: '')
logger.debug('API key set from environment')
end
@api_key = api_key.nil? || api_key.empty? ? ENV.fetch(API_KEY_ENV_NAME, API_KEY_DEFAULT) : api_key
base_url = ENV.fetch(BASE_URL_ENV_NAME, BASE_URL_DEFAULT)
@url_root = "#{base_url.chomp('/')}/products/#{@owner}/#{@url_name}/v#{@version}"
@base_url = ENV.fetch(BASE_URL_ENV_NAME, BASE_URL_DEFAULT).chomp('/')
@url_root = "#{@base_url}/products/#{@owner}/#{@url_name}/v#{@version}"
end

# Call the prediction API.
# @param input_source [Mindee::Input::Source::LocalInputSource, Mindee::Input::Source::URLInputSource]
# @param all_words [bool] Whether the full word extraction needs to be performed
# @param full_text [bool] Whether to include the full OCR text response in compatible APIs
# @param close_file [bool] Whether the file will be closed after reading
# @param cropper [bool] Whether a cropping operation will be applied
# @param opts [ParseOptions] Parse options.
# @return [Array]
def predict(input_source, all_words, full_text, close_file, cropper)
def predict(input_source, opts)
check_api_key
response = predict_req_post(
input_source,
all_words: all_words,
full_text: full_text,
close_file: close_file,
cropper: cropper
opts
)
if !response.nil? && response.respond_to?(:body)
hashed_response = JSON.parse(response.body, object_class: Hash)
Expand All @@ -76,14 +72,11 @@ def predict(input_source, all_words, full_text, close_file, cropper)

# Call the prediction API.
# @param input_source [Mindee::Input::Source::LocalInputSource, Mindee::Input::Source::URLInputSource]
# @param all_words [bool] Whether the full word extraction needs to be performed
# @param full_text [bool] Whether to include the full OCR text response in compatible APIs.
# @param close_file [bool] Whether the file will be closed after reading
# @param cropper [bool] Whether a cropping operation will be applied
# @param opts [ParseOptions, Hash] Parse options.
# @return [Array]
def predict_async(input_source, all_words, full_text, close_file, cropper)
def predict_async(input_source, opts)
check_api_key
response = document_queue_req_get(input_source, all_words, full_text, close_file, cropper)
response = document_queue_req_post(input_source, opts)
if !response.nil? && response.respond_to?(:body)
hashed_response = JSON.parse(response.body, object_class: Hash)
return [hashed_response, response.body] if ResponseValidation.valid_async_response?(response)
Expand All @@ -100,7 +93,7 @@ def predict_async(input_source, all_words, full_text, close_file, cropper)
# @return [Array]
def parse_async(job_id)
check_api_key
response = document_queue_req(job_id)
response = document_queue_req_get(job_id)
hashed_response = JSON.parse(response.body, object_class: Hash)
return [hashed_response, response.body] if ResponseValidation.valid_async_response?(response)

Expand All @@ -112,17 +105,14 @@ def parse_async(job_id)
private

# @param input_source [Mindee::Input::Source::LocalInputSource, Mindee::Input::Source::URLInputSource]
# @param all_words [bool] Whether the full word extraction needs to be performed
# @param full_text [bool] Whether to include the full OCR text response in compatible APIs.
# @param close_file [bool] Whether the file will be closed after reading
# @param cropper [bool] Whether a cropping operation will be applied
# @param opts [ParseOptions] Parse options.
# @return [Net::HTTPResponse, nil]
def predict_req_post(input_source, all_words: false, full_text: false, close_file: true, cropper: false)
def predict_req_post(input_source, opts)
uri = URI("#{@url_root}/predict")

params = {} # : Hash[Symbol | String, untyped]
params[:cropper] = 'true' if cropper
params[:full_text_ocr] = 'true' if full_text
params[:cropper] = 'true' if opts.cropper
params[:full_text_ocr] = 'true' if opts.full_text
uri.query = URI.encode_www_form(params)

headers = {
Expand All @@ -131,32 +121,33 @@ def predict_req_post(input_source, all_words: false, full_text: false, close_fil
}
req = Net::HTTP::Post.new(uri, headers)
form_data = if input_source.is_a?(Mindee::Input::Source::URLInputSource)
[['document', input_source.url]]
[['document', input_source.url]] # : Array[untyped]
else
[input_source.read_contents(close: close_file)]
[input_source.read_contents(close: opts.close_file)] # : Array[untyped]
end
form_data.push ['include_mvision', 'true'] if all_words
form_data.push ['include_mvision', 'true'] if opts.all_words

req.set_form(form_data, 'multipart/form-data')
response = nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: @request_timeout) do |http|
response = http.request(req)
return http.request(req)
end
response
raise Mindee::Errors::MindeeError, 'Could not resolve server response.'
end

# @param input_source [Mindee::Input::Source::LocalInputSource, Mindee::Input::Source::URLInputSource]
# @param all_words [bool] Whether the full word extraction needs to be performed
# @param full_text [bool] Whether to include the full OCR text response in compatible APIs.
# @param close_file [bool] Whether the file will be closed after reading
# @param cropper [bool] Whether a cropping operation will be applied
# @return [Net::HTTPResponse, nil]
def document_queue_req_get(input_source, all_words, full_text, close_file, cropper)
uri = URI("#{@url_root}/predict_async")
# @param opts [ParseOptions] Parse options.
# @return [Net::HTTPResponse]
def document_queue_req_post(input_source, opts)
uri = if opts.workflow_id
URI("#{@base_url}/workflows/#{opts.workflow_id}/predict_async")
else
URI("#{@url_root}/predict_async")
end

params = {} # : Hash[Symbol | String, untyped]
params[:cropper] = 'true' if cropper
params[:full_text_ocr] = 'true' if full_text
params[:cropper] = 'true' if opts.cropper
params[:full_text_ocr] = 'true' if opts.full_text
params[:rag] = 'true' if opts.rag
uri.query = URI.encode_www_form(params)

headers = {
Expand All @@ -165,24 +156,23 @@ def document_queue_req_get(input_source, all_words, full_text, close_file, cropp
}
req = Net::HTTP::Post.new(uri, headers)
form_data = if input_source.is_a?(Mindee::Input::Source::URLInputSource)
[['document', input_source.url]]
[['document', input_source.url]] # : Array[untyped]
else
[input_source.read_contents(close: close_file)]
[input_source.read_contents(close: opts.close_file)] # : Array[untyped]
end
form_data.push ['include_mvision', 'true'] if all_words
form_data.push ['include_mvision', 'true'] if opts.all_words

req.set_form(form_data, 'multipart/form-data')

response = nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: @request_timeout) do |http|
response = http.request(req)
return http.request(req)
end
response
raise Mindee::Errors::MindeeError, 'Could not resolve server response.'
end

# @param job_id [String]
# @return [Net::HTTPResponse, nil]
def document_queue_req(job_id)
def document_queue_req_get(job_id)
uri = URI("#{@url_root}/documents/queue/#{job_id}")

headers = {
Expand Down
2 changes: 1 addition & 1 deletion lib/mindee/http/response_validation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def self.valid_async_response?(response)
# Checks and correct the response object depending on the possible kinds of returns.
# @param response [Net::HTTPResponse]
def self.clean_request!(response)
return response if (response.code.to_i < 200) || (response.code.to_i > 302)
return response if (response.code.to_i < 200) || (response.code.to_i > 302) # : Net::HTTPResponse

return response if response.body.empty?

Expand Down
2 changes: 1 addition & 1 deletion lib/mindee/parsing/common/api_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class ApiResponse

# @param product_class [Mindee::Inference]
# @param http_response [Hash]
# @param raw_http [String]
# @param raw_http [Hash]
def initialize(product_class, http_response, raw_http)
logger.debug('Handling API response')
@raw_http = raw_http.to_s
Expand Down
1 change: 1 addition & 0 deletions lib/mindee/parsing/common/extras.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
require_relative 'extras/extras'
require_relative 'extras/cropper_extra'
require_relative 'extras/full_text_ocr_extra'
require_relative 'extras/rag_extra'
5 changes: 4 additions & 1 deletion lib/mindee/parsing/common/extras/extras.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class Extras
attr_reader :cropper
# @return [Mindee::Parsing::Common::Extras::FullTextOCRExtra, nil]
attr_reader :full_text_ocr
# @return [RAGExtra, nil]
attr_reader :rag

def initialize(raw_prediction)
if raw_prediction['cropper']
Expand All @@ -21,9 +23,10 @@ def initialize(raw_prediction)
if raw_prediction['full_text_ocr']
@full_text_ocr = Mindee::Parsing::Common::Extras::FullTextOCRExtra.new(raw_prediction['full_text_ocr'])
end
@rag = Mindee::Parsing::Common::Extras::RAGExtra.new(raw_prediction['rag']) if raw_prediction['rag']

raw_prediction.each do |key, value|
instance_variable_set("@#{key}", value) unless ['cropper', 'full_text_ocr'].include?(key)
instance_variable_set("@#{key}", value) unless ['cropper', 'full_text_ocr', 'rag'].include?(key)
end
end

Expand Down
24 changes: 24 additions & 0 deletions lib/mindee/parsing/common/extras/rag_extra.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module Mindee
module Parsing
module Common
module Extras
# Retrieval-Augmented Generation extra.
class RAGExtra
# ID of the matching document
# @return [String, nil]
attr_reader :matching_document_id

def initialize(raw_prediction)
@matching_document_id = raw_prediction['matching_document_id'] if raw_prediction['matching_document_id']
end

def to_s
@matching_document_id || ''
end
end
end
end
end
end
3 changes: 3 additions & 0 deletions lib/mindee/parsing/common/inference.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class Inference
# Whether this product has access to synchronous endpoint.
# @return [bool]
attr_reader :has_sync
# @return [Mindee::Parsing::Common::Extras::Extras] Potential Extras fields sent back along the prediction.
attr_reader :extras

@endpoint_name = nil
@endpoint_version = nil
Expand All @@ -40,6 +42,7 @@ def initialize(raw_prediction)
@is_rotation_applied = raw_prediction['is_rotation_applied']
@product = Product.new(raw_prediction['product'])
@pages = [] # : Array[Page]
@extras = Extras::Extras.new(raw_prediction['extras'])
end

# @return [String]
Expand Down
3 changes: 3 additions & 0 deletions sig/custom/net_http.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ module Net
class HTTPResponse
def self.body: -> untyped
def body: -> untyped
def []: (untyped) -> untyped
def key?: (untyped) -> bool
def code: -> String
end

class HTTPRedirection
Expand Down
Loading
Loading