Skip to content

Commit 81dddfa

Browse files
✨ add support for workflow polling (#185)
1 parent e0c832b commit 81dddfa

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+259
-156
lines changed

.github/workflows/_static-analysis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ name: Static Analysis
55

66
on:
77
workflow_call:
8+
workflow_dispatch:
89

910
jobs:
1011
linting:
@@ -18,7 +19,7 @@ jobs:
1819
- name: set up Ruby
1920
uses: ruby/setup-ruby@v1
2021
with:
21-
ruby-version: "3.0"
22+
ruby-version: "3.0.0"
2223
bundler-cache: true
2324

2425
- name: Analyse the code with Rubocop

.rubocop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ AllCops:
1010
- local_test/*
1111
- Steepfile
1212

13-
TargetRubyVersion: 3.0
13+
TargetRubyVersion: 3.0.0
1414
SuggestExtensions: false
1515

1616
Gemspec/DevelopmentDependencies:

docs/code_samples/workflow_execution.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# gem install mindee
44
#
55

6-
require_relative 'mindee'
6+
require 'mindee'
77

88
workflow_id = 'workflow-id'
99

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#
2+
# Install the Ruby client library by running:
3+
# gem install mindee
4+
#
5+
6+
require 'mindee'
7+
8+
workflow_id = 'workflow-id'
9+
10+
# Init a new client
11+
mindee_client = Mindee::Client.new
12+
13+
# Load a file from disk
14+
input_source = mindee_client.source_from_path('path/to/my/file.ext')
15+
16+
# Initialize a custom endpoint for this product
17+
custom_endpoint = mindee_client.create_endpoint(
18+
account_name: 'my-account',
19+
endpoint_name: 'my-endpoint',
20+
version: 'my-version'
21+
)
22+
23+
# Parse the file
24+
result = mindee_client.parse(
25+
input_source,
26+
Mindee::Product::Universal::Universal,
27+
endpoint: custom_endpoint,
28+
options: {
29+
rag: true,
30+
workflow_id: workflow_id
31+
}
32+
)
33+
34+
# Print a full summary of the parsed data in RST format
35+
puts result.document
36+

lib/mindee/client.rb

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ def initialize(params: {})
5454
# @!attribute delay_sec [Numeric] Delay between polling attempts. Defaults to 1.5.
5555
# @!attribute max_retries [Integer] Maximum number of retries. Defaults to 80.
5656
class ParseOptions
57-
attr_accessor :all_words, :full_text, :close_file, :page_options, :cropper,
58-
:initial_delay_sec, :delay_sec, :max_retries
57+
attr_accessor :all_words, :full_text, :close_file, :page_options, :cropper, :rag,
58+
:workflow_id, :initial_delay_sec, :delay_sec, :max_retries
5959

6060
def initialize(params: {})
6161
params = params.transform_keys(&:to_sym)
@@ -66,6 +66,8 @@ def initialize(params: {})
6666
raw_page_options = PageOptions.new(params: raw_page_options) unless raw_page_options.is_a?(PageOptions)
6767
@page_options = raw_page_options
6868
@cropper = params.fetch(:cropper, false)
69+
@rag = params.fetch(:rag, false)
70+
@workflow_id = params.fetch(:workflow_id, nil)
6971
@initial_delay_sec = params.fetch(:initial_delay_sec, 2)
7072
@delay_sec = params.fetch(:delay_sec, 1.5)
7173
@max_retries = params.fetch(:max_retries, 80)
@@ -176,13 +178,10 @@ def parse_sync(input_source, product_class, endpoint, options)
176178

177179
prediction, raw_http = endpoint.predict(
178180
input_source,
179-
options.all_words,
180-
options.full_text,
181-
options.close_file,
182-
options.cropper
181+
options
183182
)
184183

185-
Mindee::Parsing::Common::ApiResponse.new(product_class, prediction, raw_http)
184+
Mindee::Parsing::Common::ApiResponse.new(product_class, prediction, raw_http.to_s)
186185
end
187186

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

217218
prediction, raw_http = endpoint.predict_async(
218219
input_source,
219-
opts.all_words,
220-
opts.full_text,
221-
opts.close_file,
222-
opts.cropper
220+
opts
223221
)
224-
Mindee::Parsing::Common::ApiResponse.new(product_class, prediction, raw_http)
222+
Mindee::Parsing::Common::ApiResponse.new(product_class, prediction, raw_http.to_json)
225223
end
226224

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

242240
# Enqueue a document for async parsing and automatically try to retrieve it
@@ -261,6 +259,8 @@ def parse_queued(job_id, product_class, endpoint: nil)
261259
# - `:on_min_pages` [Integer] Apply the operation only if the document has at least this many pages.
262260
# * `:cropper` [bool, nil] Whether to include cropper results for each page.
263261
# This performs a cropping operation on the server and will increase response time.
262+
# * `:rag` [bool] Whether to enable Retrieval-Augmented Generation. Only works if a Workflow ID is provided.
263+
# * `:workflow_id` [String, nil] ID of the workflow to use.
264264
# * `:initial_delay_sec` [Numeric] Initial delay before polling. Defaults to 2.
265265
# * `:delay_sec` [Numeric] Delay between polling attempts. Defaults to 1.5.
266266
# * `:max_retries` [Integer] Maximum number of retries. Defaults to 80.

lib/mindee/http/endpoint.rb

Lines changed: 37 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ class Endpoint
3434
attr_reader :request_timeout
3535
# @return [String]
3636
attr_reader :url_root
37+
# @return [String]
38+
attr_reader :base_url
3739

3840
def initialize(owner, url_name, version, api_key: '')
3941
@owner = owner
@@ -44,25 +46,19 @@ def initialize(owner, url_name, version, api_key: '')
4446
logger.debug('API key set from environment')
4547
end
4648
@api_key = api_key.nil? || api_key.empty? ? ENV.fetch(API_KEY_ENV_NAME, API_KEY_DEFAULT) : api_key
47-
base_url = ENV.fetch(BASE_URL_ENV_NAME, BASE_URL_DEFAULT)
48-
@url_root = "#{base_url.chomp('/')}/products/#{@owner}/#{@url_name}/v#{@version}"
49+
@base_url = ENV.fetch(BASE_URL_ENV_NAME, BASE_URL_DEFAULT).chomp('/')
50+
@url_root = "#{@base_url}/products/#{@owner}/#{@url_name}/v#{@version}"
4951
end
5052

5153
# Call the prediction API.
5254
# @param input_source [Mindee::Input::Source::LocalInputSource, Mindee::Input::Source::URLInputSource]
53-
# @param all_words [bool] Whether the full word extraction needs to be performed
54-
# @param full_text [bool] Whether to include the full OCR text response in compatible APIs
55-
# @param close_file [bool] Whether the file will be closed after reading
56-
# @param cropper [bool] Whether a cropping operation will be applied
55+
# @param opts [ParseOptions] Parse options.
5756
# @return [Array]
58-
def predict(input_source, all_words, full_text, close_file, cropper)
57+
def predict(input_source, opts)
5958
check_api_key
6059
response = predict_req_post(
6160
input_source,
62-
all_words: all_words,
63-
full_text: full_text,
64-
close_file: close_file,
65-
cropper: cropper
61+
opts
6662
)
6763
if !response.nil? && response.respond_to?(:body)
6864
hashed_response = JSON.parse(response.body, object_class: Hash)
@@ -76,14 +72,11 @@ def predict(input_source, all_words, full_text, close_file, cropper)
7672

7773
# Call the prediction API.
7874
# @param input_source [Mindee::Input::Source::LocalInputSource, Mindee::Input::Source::URLInputSource]
79-
# @param all_words [bool] Whether the full word extraction needs to be performed
80-
# @param full_text [bool] Whether to include the full OCR text response in compatible APIs.
81-
# @param close_file [bool] Whether the file will be closed after reading
82-
# @param cropper [bool] Whether a cropping operation will be applied
75+
# @param opts [ParseOptions, Hash] Parse options.
8376
# @return [Array]
84-
def predict_async(input_source, all_words, full_text, close_file, cropper)
77+
def predict_async(input_source, opts)
8578
check_api_key
86-
response = document_queue_req_get(input_source, all_words, full_text, close_file, cropper)
79+
response = document_queue_req_post(input_source, opts)
8780
if !response.nil? && response.respond_to?(:body)
8881
hashed_response = JSON.parse(response.body, object_class: Hash)
8982
return [hashed_response, response.body] if ResponseValidation.valid_async_response?(response)
@@ -100,7 +93,7 @@ def predict_async(input_source, all_words, full_text, close_file, cropper)
10093
# @return [Array]
10194
def parse_async(job_id)
10295
check_api_key
103-
response = document_queue_req(job_id)
96+
response = document_queue_req_get(job_id)
10497
hashed_response = JSON.parse(response.body, object_class: Hash)
10598
return [hashed_response, response.body] if ResponseValidation.valid_async_response?(response)
10699

@@ -112,17 +105,14 @@ def parse_async(job_id)
112105
private
113106

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

123113
params = {} # : Hash[Symbol | String, untyped]
124-
params[:cropper] = 'true' if cropper
125-
params[:full_text_ocr] = 'true' if full_text
114+
params[:cropper] = 'true' if opts.cropper
115+
params[:full_text_ocr] = 'true' if opts.full_text
126116
uri.query = URI.encode_www_form(params)
127117

128118
headers = {
@@ -131,32 +121,33 @@ def predict_req_post(input_source, all_words: false, full_text: false, close_fil
131121
}
132122
req = Net::HTTP::Post.new(uri, headers)
133123
form_data = if input_source.is_a?(Mindee::Input::Source::URLInputSource)
134-
[['document', input_source.url]]
124+
[['document', input_source.url]] # : Array[untyped]
135125
else
136-
[input_source.read_contents(close: close_file)]
126+
[input_source.read_contents(close: opts.close_file)] # : Array[untyped]
137127
end
138-
form_data.push ['include_mvision', 'true'] if all_words
128+
form_data.push ['include_mvision', 'true'] if opts.all_words
139129

140130
req.set_form(form_data, 'multipart/form-data')
141-
response = nil
142131
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: @request_timeout) do |http|
143-
response = http.request(req)
132+
return http.request(req)
144133
end
145-
response
134+
raise Mindee::Errors::MindeeError, 'Could not resolve server response.'
146135
end
147136

148137
# @param input_source [Mindee::Input::Source::LocalInputSource, Mindee::Input::Source::URLInputSource]
149-
# @param all_words [bool] Whether the full word extraction needs to be performed
150-
# @param full_text [bool] Whether to include the full OCR text response in compatible APIs.
151-
# @param close_file [bool] Whether the file will be closed after reading
152-
# @param cropper [bool] Whether a cropping operation will be applied
153-
# @return [Net::HTTPResponse, nil]
154-
def document_queue_req_get(input_source, all_words, full_text, close_file, cropper)
155-
uri = URI("#{@url_root}/predict_async")
138+
# @param opts [ParseOptions] Parse options.
139+
# @return [Net::HTTPResponse]
140+
def document_queue_req_post(input_source, opts)
141+
uri = if opts.workflow_id
142+
URI("#{@base_url}/workflows/#{opts.workflow_id}/predict_async")
143+
else
144+
URI("#{@url_root}/predict_async")
145+
end
156146

157147
params = {} # : Hash[Symbol | String, untyped]
158-
params[:cropper] = 'true' if cropper
159-
params[:full_text_ocr] = 'true' if full_text
148+
params[:cropper] = 'true' if opts.cropper
149+
params[:full_text_ocr] = 'true' if opts.full_text
150+
params[:rag] = 'true' if opts.rag
160151
uri.query = URI.encode_www_form(params)
161152

162153
headers = {
@@ -165,24 +156,23 @@ def document_queue_req_get(input_source, all_words, full_text, close_file, cropp
165156
}
166157
req = Net::HTTP::Post.new(uri, headers)
167158
form_data = if input_source.is_a?(Mindee::Input::Source::URLInputSource)
168-
[['document', input_source.url]]
159+
[['document', input_source.url]] # : Array[untyped]
169160
else
170-
[input_source.read_contents(close: close_file)]
161+
[input_source.read_contents(close: opts.close_file)] # : Array[untyped]
171162
end
172-
form_data.push ['include_mvision', 'true'] if all_words
163+
form_data.push ['include_mvision', 'true'] if opts.all_words
173164

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

176-
response = nil
177167
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: @request_timeout) do |http|
178-
response = http.request(req)
168+
return http.request(req)
179169
end
180-
response
170+
raise Mindee::Errors::MindeeError, 'Could not resolve server response.'
181171
end
182172

183173
# @param job_id [String]
184174
# @return [Net::HTTPResponse, nil]
185-
def document_queue_req(job_id)
175+
def document_queue_req_get(job_id)
186176
uri = URI("#{@url_root}/documents/queue/#{job_id}")
187177

188178
headers = {

lib/mindee/http/response_validation.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def self.valid_async_response?(response)
4242
# Checks and correct the response object depending on the possible kinds of returns.
4343
# @param response [Net::HTTPResponse]
4444
def self.clean_request!(response)
45-
return response if (response.code.to_i < 200) || (response.code.to_i > 302)
45+
return response if (response.code.to_i < 200) || (response.code.to_i > 302) # : Net::HTTPResponse
4646

4747
return response if response.body.empty?
4848

lib/mindee/parsing/common/api_response.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class ApiResponse
3535

3636
# @param product_class [Mindee::Inference]
3737
# @param http_response [Hash]
38-
# @param raw_http [String]
38+
# @param raw_http [Hash]
3939
def initialize(product_class, http_response, raw_http)
4040
logger.debug('Handling API response')
4141
@raw_http = raw_http.to_s

lib/mindee/parsing/common/document.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def inject_full_text_ocr(raw_prediction)
6969

7070
full_text_ocr = String.new
7171
raw_prediction.dig('inference', 'pages').each do |page|
72-
full_text_ocr << (page['extras']['full_text_ocr']['content'])
72+
full_text_ocr << page['extras']['full_text_ocr']['content']
7373
end
7474
artificial_text_obj = { 'content' => full_text_ocr }
7575
if @extras.nil? || @extras.empty?

lib/mindee/parsing/common/extras.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
require_relative 'extras/extras'
44
require_relative 'extras/cropper_extra'
55
require_relative 'extras/full_text_ocr_extra'
6+
require_relative 'extras/rag_extra'

0 commit comments

Comments
 (0)