ruby_decision_model 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +104 -0
- data/README.md +338 -14
- data/lib/ruby_decision_model/client.rb +193 -86
- data/lib/ruby_decision_model/errors.rb +13 -4
- data/lib/ruby_decision_model/images.rb +33 -0
- data/lib/ruby_decision_model/providers/base.rb +252 -0
- data/lib/ruby_decision_model/providers/cloudflare.rb +119 -0
- data/lib/ruby_decision_model/providers/databricks.rb +87 -0
- data/lib/ruby_decision_model/providers/open_router.rb +50 -0
- data/lib/ruby_decision_model/providers/openai.rb +237 -0
- data/lib/ruby_decision_model/providers/perplexity.rb +73 -0
- data/lib/ruby_decision_model/providers/system_one.rb +90 -0
- data/lib/ruby_decision_model/providers/typesafe.rb +39 -0
- data/lib/ruby_decision_model/providers.rb +78 -0
- data/lib/ruby_decision_model/response.rb +22 -2
- data/lib/ruby_decision_model/retry_policy.rb +155 -0
- data/lib/ruby_decision_model/version.rb +1 -1
- data/lib/ruby_decision_model.rb +15 -0
- metadata +20 -6
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module RubyDecisionModel
|
|
6
|
+
# Retry rules shared by every provider. Defaults follow the official
|
|
7
|
+
# Typesafe SDKs: two retries after the initial attempt, exponential
|
|
8
|
+
# backoff from 0.5s capped at 5s with up to 25% jitter subtracted,
|
|
9
|
+
# Retry-After honored up to 60s, and a 30s total budget across attempts.
|
|
10
|
+
class RetryPolicy < Data.define(
|
|
11
|
+
:max_retries,
|
|
12
|
+
:backoff_initial,
|
|
13
|
+
:backoff_max,
|
|
14
|
+
:backoff_jitter,
|
|
15
|
+
:http_statuses,
|
|
16
|
+
:respect_retry_after,
|
|
17
|
+
:max_retry_after,
|
|
18
|
+
:retry_connection_errors,
|
|
19
|
+
:retry_timeouts,
|
|
20
|
+
:total_timeout
|
|
21
|
+
)
|
|
22
|
+
DEFAULT_STATUSES = ([408, 429] + (500..599).to_a).freeze
|
|
23
|
+
|
|
24
|
+
TIMEOUT_EXCEPTIONS = [Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout].freeze
|
|
25
|
+
CONNECTION_EXCEPTIONS = [
|
|
26
|
+
Errno::ECONNRESET,
|
|
27
|
+
Errno::ECONNREFUSED,
|
|
28
|
+
Errno::ECONNABORTED,
|
|
29
|
+
Errno::EHOSTUNREACH,
|
|
30
|
+
Errno::ENETUNREACH,
|
|
31
|
+
Errno::EPIPE,
|
|
32
|
+
SocketError,
|
|
33
|
+
IOError,
|
|
34
|
+
OpenSSL::SSL::SSLError
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
def initialize(max_retries: 2, backoff_initial: 0.5, backoff_max: 5.0, backoff_jitter: 0.25,
|
|
38
|
+
http_statuses: DEFAULT_STATUSES, respect_retry_after: true, max_retry_after: 60.0,
|
|
39
|
+
retry_connection_errors: true, retry_timeouts: true, total_timeout: 30.0)
|
|
40
|
+
super
|
|
41
|
+
validate!
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Accepts a RetryPolicy, a Hash of overrides, or nil (defaults).
|
|
45
|
+
def self.from(value)
|
|
46
|
+
case value
|
|
47
|
+
when RetryPolicy then value
|
|
48
|
+
when nil then new
|
|
49
|
+
when Hash then new(**value.transform_keys(&:to_sym))
|
|
50
|
+
else
|
|
51
|
+
raise ConfigurationError, "retry must be a RetryPolicy or a Hash of overrides, got #{value.class}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def retryable_status?(status)
|
|
56
|
+
http_statuses.include?(status)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def timeout_exception?(exception)
|
|
60
|
+
TIMEOUT_EXCEPTIONS.any? { |klass| exception.is_a?(klass) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def connection_exception?(exception)
|
|
64
|
+
CONNECTION_EXCEPTIONS.any? { |klass| exception.is_a?(klass) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def retryable_exception?(exception)
|
|
68
|
+
return retry_timeouts if timeout_exception?(exception)
|
|
69
|
+
return retry_connection_errors if connection_exception?(exception)
|
|
70
|
+
|
|
71
|
+
false
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Seconds to wait before the retry numbered `retry_number` (0 for the
|
|
75
|
+
# first retry). `random` returns a Float in 0...1 and exists so tests can
|
|
76
|
+
# pin the jitter.
|
|
77
|
+
def backoff(retry_number, random: -> { rand })
|
|
78
|
+
base = [backoff_initial * (2**retry_number), backoff_max].min
|
|
79
|
+
[base * (1.0 - (backoff_jitter * random.call)), 0.0].max
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Delay before the next retry: the server's Retry-After when present and
|
|
83
|
+
# honored (clamped to max_retry_after), otherwise the computed backoff.
|
|
84
|
+
def delay(retry_number, headers: {}, random: -> { rand })
|
|
85
|
+
hinted = respect_retry_after ? retry_after_seconds(headers) : nil
|
|
86
|
+
return [hinted, max_retry_after].min if hinted
|
|
87
|
+
|
|
88
|
+
backoff(retry_number, random: random)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Reads retry-after-ms (preferred) or Retry-After (seconds or HTTP date).
|
|
92
|
+
# Header names are matched case-insensitively. Returns nil when absent
|
|
93
|
+
# or unparseable.
|
|
94
|
+
def retry_after_seconds(headers)
|
|
95
|
+
return nil unless headers.is_a?(Hash)
|
|
96
|
+
|
|
97
|
+
ms = header_value(headers, "retry-after-ms")
|
|
98
|
+
if ms
|
|
99
|
+
parsed = Float(ms, exception: false)
|
|
100
|
+
return parsed / 1000.0 if parsed && parsed >= 0
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
raw = header_value(headers, "retry-after")
|
|
104
|
+
return nil if raw.nil?
|
|
105
|
+
|
|
106
|
+
seconds = Float(raw, exception: false)
|
|
107
|
+
return seconds if seconds && seconds >= 0
|
|
108
|
+
|
|
109
|
+
begin
|
|
110
|
+
[Time.httpdate(raw.to_s) - Time.now, 0.0].max
|
|
111
|
+
rescue ArgumentError
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def validate!
|
|
119
|
+
unless max_retries.is_a?(Integer) && max_retries >= 0
|
|
120
|
+
raise ConfigurationError, "max_retries must be a non-negative Integer, got #{max_retries.inspect}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
{ backoff_initial: backoff_initial, backoff_max: backoff_max, max_retry_after: max_retry_after }.each do |name, value|
|
|
124
|
+
next if finite_non_negative?(value)
|
|
125
|
+
|
|
126
|
+
raise ConfigurationError, "#{name} must be a finite non-negative number, got #{value.inspect}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
unless finite_non_negative?(backoff_jitter) && backoff_jitter <= 1.0
|
|
130
|
+
raise ConfigurationError, "backoff_jitter must be a number between 0 and 1, got #{backoff_jitter.inspect}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
unless total_timeout.nil? || finite_non_negative?(total_timeout)
|
|
134
|
+
raise ConfigurationError, "total_timeout must be nil or a finite non-negative number, got #{total_timeout.inspect}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
return if http_statuses.respond_to?(:include?)
|
|
138
|
+
|
|
139
|
+
raise ConfigurationError, "http_statuses must respond to include?, got #{http_statuses.inspect}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def finite_non_negative?(value)
|
|
143
|
+
value.is_a?(Numeric) && value.finite? && value >= 0
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def header_value(headers, name)
|
|
147
|
+
headers.each do |key, value|
|
|
148
|
+
next unless key.to_s.casecmp?(name)
|
|
149
|
+
|
|
150
|
+
return value.is_a?(Array) ? value.first : value
|
|
151
|
+
end
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
data/lib/ruby_decision_model.rb
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
3
6
|
require_relative "ruby_decision_model/version"
|
|
4
7
|
require_relative "ruby_decision_model/errors"
|
|
5
8
|
require_relative "ruby_decision_model/questions"
|
|
9
|
+
require_relative "ruby_decision_model/images"
|
|
6
10
|
require_relative "ruby_decision_model/answers"
|
|
7
11
|
require_relative "ruby_decision_model/response"
|
|
12
|
+
require_relative "ruby_decision_model/providers"
|
|
13
|
+
require_relative "ruby_decision_model/retry_policy"
|
|
8
14
|
require_relative "ruby_decision_model/client"
|
|
9
15
|
|
|
10
16
|
module RubyDecisionModel
|
|
17
|
+
class << self
|
|
18
|
+
# A memoized default client built from the environment. Reset with
|
|
19
|
+
# `RubyDecisionModel.client = nil`.
|
|
20
|
+
def client
|
|
21
|
+
@client ||= Client.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
attr_writer :client
|
|
25
|
+
end
|
|
11
26
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_decision_model
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Obie Fernandez
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest
|
|
@@ -40,9 +40,11 @@ dependencies:
|
|
|
40
40
|
version: '13.0'
|
|
41
41
|
description: Decision models answer typed questions about a state instead of generating
|
|
42
42
|
text. ruby_decision_model builds noul (yes/no probability), choice, and score questions,
|
|
43
|
-
posts them with a state to a
|
|
44
|
-
|
|
45
|
-
and
|
|
43
|
+
posts them with a state and optional images to a decision-model provider (OpenRouter,
|
|
44
|
+
Typesafe, OpenAI, Cloudflare, Perplexity, Databricks, or any System One server such
|
|
45
|
+
as Ollama), and returns the same normalized answers with probabilities, confidence,
|
|
46
|
+
legends, and usage whichever vendor served them. Retries follow the official Typesafe
|
|
47
|
+
SDKs.
|
|
46
48
|
email:
|
|
47
49
|
- obiefernandez@gmail.com
|
|
48
50
|
executables: []
|
|
@@ -56,8 +58,19 @@ files:
|
|
|
56
58
|
- lib/ruby_decision_model/answers.rb
|
|
57
59
|
- lib/ruby_decision_model/client.rb
|
|
58
60
|
- lib/ruby_decision_model/errors.rb
|
|
61
|
+
- lib/ruby_decision_model/images.rb
|
|
62
|
+
- lib/ruby_decision_model/providers.rb
|
|
63
|
+
- lib/ruby_decision_model/providers/base.rb
|
|
64
|
+
- lib/ruby_decision_model/providers/cloudflare.rb
|
|
65
|
+
- lib/ruby_decision_model/providers/databricks.rb
|
|
66
|
+
- lib/ruby_decision_model/providers/open_router.rb
|
|
67
|
+
- lib/ruby_decision_model/providers/openai.rb
|
|
68
|
+
- lib/ruby_decision_model/providers/perplexity.rb
|
|
69
|
+
- lib/ruby_decision_model/providers/system_one.rb
|
|
70
|
+
- lib/ruby_decision_model/providers/typesafe.rb
|
|
59
71
|
- lib/ruby_decision_model/questions.rb
|
|
60
72
|
- lib/ruby_decision_model/response.rb
|
|
73
|
+
- lib/ruby_decision_model/retry_policy.rb
|
|
61
74
|
- lib/ruby_decision_model/version.rb
|
|
62
75
|
homepage: https://github.com/obie/ruby_decision_model
|
|
63
76
|
licenses:
|
|
@@ -83,5 +96,6 @@ requirements: []
|
|
|
83
96
|
rubygems_version: 3.5.11
|
|
84
97
|
signing_key:
|
|
85
98
|
specification_version: 4
|
|
86
|
-
summary:
|
|
99
|
+
summary: 'The decision-model interface for Ruby: one client for Jev, gpt-6-luna, Clef,
|
|
100
|
+
pplx-decider, and more'
|
|
87
101
|
test_files: []
|