Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion .github/workflows/_static-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ name: Static Analysis

on:
workflow_call:
workflow_dispatch:

jobs:
linting:
Expand All @@ -18,7 +19,7 @@ jobs:
- name: set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.0"
ruby-version: "3.0.0"
bundler-cache: true

- name: Analyse the code with Rubocop
Expand Down
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ AllCops:
- local_test/*
- Steepfile

TargetRubyVersion: 3.0
TargetRubyVersion: 3.0.0
SuggestExtensions: false

Gemspec/DevelopmentDependencies:
Expand Down
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
2 changes: 1 addition & 1 deletion lib/mindee/parsing/common/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def inject_full_text_ocr(raw_prediction)

full_text_ocr = String.new
raw_prediction.dig('inference', 'pages').each do |page|
full_text_ocr << (page['extras']['full_text_ocr']['content'])
full_text_ocr << page['extras']['full_text_ocr']['content']
end
artificial_text_obj = { 'content' => full_text_ocr }
if @extras.nil? || @extras.empty?
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
Loading