Class: Pangram::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/pangram/client.rb

Overview

Client for Pangram text detection, bulk, file upload, and plagiarism APIs.

Constant Summary collapse

TEXT_API_ENDPOINT =

Base URL for model discovery, text prediction, and bulk jobs.

'https://text.external-api.pangram.com'
FILE_UPLOAD_API_ENDPOINT =

Base URL for multipart document prediction.

'https://file-external.api.pangram.com'
PLAGIARISM_API_ENDPOINT =

Base URL for plagiarism detection.

'https://plagiarism.api.pangram.com'
ASYNC_SUCCESS_STAGE =

Terminal success stage for asynchronous text prediction.

'STAGE_SUCCESS'
ASYNC_FAILED_STAGE =

Terminal failure stage for asynchronous text prediction.

'STAGE_FAILED'
BULK_TERMINAL_STATUSES =

Terminal statuses for asynchronous bulk jobs.

%w[succeeded failed partial].freeze
DEFAULT_PREDICT_TIMEOUT =

Default total prediction deadline in seconds.

300
DEFAULT_BULK_TIMEOUT =

Default total bulk polling deadline in seconds.

3600
DEFAULT_POLL_INTERVAL =

Default delay between asynchronous status requests in seconds.

0.5
MIN_POLL_INTERVAL =

Smallest allowed polling delay and request timeout in seconds.

0.1
HTTP_REQUEST_TIMEOUT =

Maximum timeout for ordinary API requests in seconds.

10
PLAGIARISM_TIMEOUT =

Timeout for plagiarism requests in seconds.

90
MAX_BULK_PAGE_LIMIT =

Largest results page accepted by the Bulk API.

1000
RETRYABLE_STATUSES =

Transient HTTP statuses worth retrying while polling or paginating.

[408, 429, 500, 502, 503, 504].freeze
MODEL_SELECTION_DEPRECATION_MESSAGE =

Warning emitted while omitted model selectors remain backward-compatible.

'Omitting model is deprecated. Pass model: "default" or another ' \
'identifier returned by list_models. Model will be required after ' \
'September 30, 2026.'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(api_key: nil) ⇒ Client

Returns a new instance of Client.



62
63
64
65
66
67
68
69
70
71
# File 'lib/pangram/client.rb', line 62

def initialize(api_key: nil)
  @api_key = (api_key.nil? ? ENV.fetch('PANGRAM_API_KEY', nil) : api_key).to_s.strip
  if @api_key.empty?
    raise AuthenticationError, 'API key is required. Set PANGRAM_API_KEY or pass api_key: to Pangram.new.'
  end

  @text_connection = build_connection(TEXT_API_ENDPOINT)
  @file_connection = build_connection(FILE_UPLOAD_API_ENDPOINT, multipart: true)
  @plagiarism_connection = build_connection(PLAGIARISM_API_ENDPOINT)
end

Instance Attribute Details

#api_keyObject (readonly)

Returns the value of attribute api_key.



60
61
62
# File 'lib/pangram/client.rb', line 60

def api_key
  @api_key
end

Instance Method Details

#batch_predict(text_batch, model: nil) ⇒ Object

Deprecated sequential compatibility helper. Prefer submit_bulk.



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/pangram/client.rb', line 274

def batch_predict(text_batch, model: nil)
  deprecate(:batch_predict,
            'batch_predict is deprecated; use submit_bulk instead. ' \
            'This method may be removed after August 1, 2026.')
  normalized_model = resolve_model(model)
  text_batch.map do |text|
    predict_with_resolved_model(
      text,
      model: normalized_model,
      public_dashboard_link: false,
      timeout: DEFAULT_PREDICT_TIMEOUT,
      poll_interval: DEFAULT_POLL_INTERVAL
    )
  end
end

#check_plagiarism(text) ⇒ Object

Check text for potential plagiarism against online sources.



248
249
250
251
252
253
254
255
256
257
258
# File 'lib/pangram/client.rb', line 248

def check_plagiarism(text)
  response = json_request(
    @plagiarism_connection,
    :post,
    '/',
    body: { text: text, source: "ruby_sdk_#{VERSION}" },
    timeout: PLAGIARISM_TIMEOUT,
    operation: 'checking plagiarism'
  )
  ensure_hash_response!(response, 'plagiarism response')
end

#get_bulk_items(bulk_id, offset: 0, limit: 100) ⇒ Object

Fetch one page of Bulk API item metadata.



143
144
145
146
147
148
149
150
151
152
153
# File 'lib/pangram/client.rb', line 143

def get_bulk_items(bulk_id, offset: 0, limit: 100)
  response = json_request(
    @text_connection,
    :get,
    "/bulk/#{escape_path_segment(bulk_id, 'bulk_id')}/items",
    params: { offset: offset, limit: limit },
    timeout: HTTP_REQUEST_TIMEOUT,
    operation: 'fetching bulk items'
  )
  ensure_hash_response!(response, 'bulk items response')
end

#get_bulk_results(bulk_id, page_size: MAX_BULK_PAGE_LIMIT, timeout: DEFAULT_BULK_TIMEOUT) ⇒ Object

Materialize every Bulk API results page in one Hash.



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/pangram/client.rb', line 169

def get_bulk_results(bulk_id, page_size: MAX_BULK_PAGE_LIMIT, timeout: DEFAULT_BULK_TIMEOUT)
  validate_bulk_results_options!(page_size, timeout)
  deadline = monotonic_time + timeout
  offset = 0
  total_items = nil
  response_bulk_id = bulk_id
  items = []
  failed_items = []

  while total_items.nil? || offset < total_items
    page = fetch_bulk_results_page(bulk_id, offset, page_size, deadline, timeout)
    validate_bulk_results_page!(page)
    # Lock in the first page's counters; later pages only contribute items.
    if total_items.nil?
      total_items = page['total_items']
      response_bulk_id = page['bulk_id']
    end
    items.concat(page['items'])
    failed_items.concat(page['failed_items'])
    offset += page_size
  end

  {
    'bulk_id' => response_bulk_id,
    'total_items' => total_items || 0,
    'items' => items,
    'failed_items' => failed_items
  }
end

#get_bulk_results_page(bulk_id, offset: 0, limit: 100, timeout: HTTP_REQUEST_TIMEOUT) ⇒ Object

Fetch one page of Bulk API results.



156
157
158
159
160
161
162
163
164
165
166
# File 'lib/pangram/client.rb', line 156

def get_bulk_results_page(bulk_id, offset: 0, limit: 100, timeout: HTTP_REQUEST_TIMEOUT)
  response = json_request(
    @text_connection,
    :get,
    "/bulk/#{escape_path_segment(bulk_id, 'bulk_id')}/results",
    params: { offset: offset, limit: limit },
    timeout: timeout,
    operation: 'fetching bulk results'
  )
  ensure_hash_response!(response, 'bulk results response')
end

#get_bulk_status(bulk_id) ⇒ Object

Fetch the status and counters for a Bulk API job.



138
139
140
# File 'lib/pangram/client.rb', line 138

def get_bulk_status(bulk_id)
  fetch_bulk_status(bulk_id, HTTP_REQUEST_TIMEOUT)
end

#inspectObject

Redacted inspection so the API key never leaks into logs or error reports.



74
75
76
# File 'lib/pangram/client.rb', line 74

def inspect
  "#<#{self.class.name} api_key=[FILTERED]>"
end

#list_modelsObject

Return the ordered model selectors available to this API key.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/pangram/client.rb', line 79

def list_models
  response = json_request(
    @text_connection,
    :get,
    '/models',
    headers: auth_headers,
    timeout: HTTP_REQUEST_TIMEOUT,
    operation: 'listing models'
  )
  models = response['models'] if response.is_a?(Hash)

  invalid_response!('model catalog', response) unless valid_model_catalog?(models)

  models.dup
end

#predict(text, model: nil, public_dashboard_link: false, timeout: DEFAULT_PREDICT_TIMEOUT, poll_interval: DEFAULT_POLL_INTERVAL) ⇒ Object

Submit a text task, poll it to completion, and return the successful API payload.



96
97
98
99
100
101
102
103
104
105
# File 'lib/pangram/client.rb', line 96

def predict(text, model: nil, public_dashboard_link: false, timeout: DEFAULT_PREDICT_TIMEOUT,
            poll_interval: DEFAULT_POLL_INTERVAL)
  predict_with_resolved_model(
    text,
    model: resolve_model(model),
    public_dashboard_link: public_dashboard_link,
    timeout: timeout,
    poll_interval: poll_interval
  )
end

#predict_file(file_path, public_dashboard_link: false, timeout: DEFAULT_PREDICT_TIMEOUT) ⇒ Object

Upload one file for AI detection and return its result.



227
228
229
230
231
232
# File 'lib/pangram/client.rb', line 227

def predict_file(file_path, public_dashboard_link: false, timeout: DEFAULT_PREDICT_TIMEOUT)
  results = predict_files([file_path], public_dashboard_link: public_dashboard_link, timeout: timeout)
  invalid_response!('file upload response', results) if results.empty?

  results.first
end

#predict_files(file_paths, public_dashboard_link: false, timeout: DEFAULT_PREDICT_TIMEOUT) ⇒ Object

Upload one or more files for AI detection.



235
236
237
238
239
240
241
242
243
244
245
# File 'lib/pangram/client.rb', line 235

def predict_files(file_paths, public_dashboard_link: false, timeout: DEFAULT_PREDICT_TIMEOUT)
  validate_file_options!(file_paths, timeout)

  opened_files = open_upload_files(file_paths)
  response = upload_files(opened_files, public_dashboard_link, timeout)
  invalid_response!('file upload response', response) unless valid_file_upload_response?(response)

  response
ensure
  opened_files&.each(&:close)
end

#predict_short(text, model: nil) ⇒ Object

Deprecated compatibility alias for predict.



261
262
263
264
265
266
267
268
269
270
271
# File 'lib/pangram/client.rb', line 261

def predict_short(text, model: nil)
  deprecate(:predict_short,
            'predict_short is deprecated; use predict instead. This method may be removed after August 1, 2026.')
  predict_with_resolved_model(
    text,
    model: resolve_model(model),
    public_dashboard_link: false,
    timeout: DEFAULT_PREDICT_TIMEOUT,
    poll_interval: DEFAULT_POLL_INTERVAL
  )
end

Predict text and request a public Pangram dashboard link.



108
109
110
111
112
113
114
115
116
117
# File 'lib/pangram/client.rb', line 108

def predict_with_dashboard_link(text, model: nil, timeout: DEFAULT_PREDICT_TIMEOUT,
                                poll_interval: DEFAULT_POLL_INTERVAL)
  predict_with_resolved_model(
    text,
    model: resolve_model(model),
    public_dashboard_link: true,
    timeout: timeout,
    poll_interval: poll_interval
  )
end

#submit_bulk(text: nil, items: nil, model: nil) ⇒ Object

Submit a Bulk API job. Provide exactly one of text or items.



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/pangram/client.rb', line 120

def submit_bulk(text: nil, items: nil, model: nil)
  payload = bulk_payload(text, items)
  normalized_model = resolve_model(model)
  payload[:model] = normalized_model unless normalized_model.nil?

  response = json_request(
    @text_connection,
    :post,
    '/bulk',
    body: payload,
    expected_statuses: [202],
    timeout: HTTP_REQUEST_TIMEOUT,
    operation: 'submitting bulk job'
  )
  ensure_hash_response!(response, 'bulk response')
end

#wait_for_bulk(bulk_id, timeout: DEFAULT_BULK_TIMEOUT, poll_interval: DEFAULT_POLL_INTERVAL) ⇒ Object

Poll a Bulk API job until its status is succeeded, failed, or partial.



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/pangram/client.rb', line 200

def wait_for_bulk(bulk_id, timeout: DEFAULT_BULK_TIMEOUT, poll_interval: DEFAULT_POLL_INTERVAL)
  validate_polling_options!(timeout, poll_interval)
  deadline = monotonic_time + timeout
  interval = [MIN_POLL_INTERVAL, poll_interval].max
  last_status = nil

  loop do
    raise bulk_timeout_error(bulk_id, timeout, last_status) if monotonic_time >= deadline

    begin
      response = fetch_bulk_status(bulk_id, request_timeout(deadline))
    rescue NetworkError, APIError => e
      raise if e.is_a?(APIError) && !RETRYABLE_STATUSES.include?(e.status)
      raise bulk_timeout_error(bulk_id, timeout, last_status) if monotonic_time >= deadline

      sleep_before_retry(deadline, interval)
      next
    end

    last_status = response['status']
    return response if BULK_TERMINAL_STATUSES.include?(last_status)

    sleep_before_retry(deadline, interval)
  end
end