typesafe-sdk-ruby 0.6.0 → 0.7.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 +81 -1
- data/LICENSE +1 -0
- data/README.md +365 -94
- data/docs/COMPATIBILITY.md +119 -0
- data/docs/RELEASING.md +38 -0
- data/docs/UPSTREAM_ISSUES.md +74 -0
- data/lib/typesafe/sdk/assets.rb +326 -0
- data/lib/typesafe/sdk/client.rb +98 -30
- data/lib/typesafe/sdk/compression.rb +50 -0
- data/lib/typesafe/sdk/errors.rb +35 -6
- data/lib/typesafe/sdk/http.rb +58 -9
- data/lib/typesafe/sdk/logging.rb +19 -7
- data/lib/typesafe/sdk/questions.rb +74 -35
- data/lib/typesafe/sdk/resources/models.rb +6 -4
- data/lib/typesafe/sdk/retry.rb +4 -4
- data/lib/typesafe/sdk/types.rb +69 -11
- data/lib/typesafe/sdk/version.rb +3 -2
- data/lib/typesafe-sdk-ruby.rb +30 -0
- metadata +17 -7
|
@@ -2,63 +2,102 @@
|
|
|
2
2
|
|
|
3
3
|
module Typesafe
|
|
4
4
|
module SDK
|
|
5
|
-
# Builders and validation for the
|
|
5
|
+
# Builders and validation for the noul, score, choice, and box question types.
|
|
6
6
|
module Questions
|
|
7
|
+
MAX_SCORE_LEVELS = 10
|
|
8
|
+
MAX_CHOICE_OPTIONS = 255
|
|
9
|
+
|
|
7
10
|
module_function
|
|
8
11
|
|
|
9
|
-
# Create a yes/no question
|
|
10
|
-
#
|
|
11
|
-
# @param
|
|
12
|
-
# @param criteria [Hash, nil] optional descriptions of the yes and no outcomes.
|
|
12
|
+
# Create a yes/no question. Supply instructions or at least one outcome description.
|
|
13
|
+
# @param instructions [String, Hash, Array, Image, TextWithAssets, nil]
|
|
14
|
+
# @param criteria [Hash, nil] optional true/false outcome descriptions.
|
|
13
15
|
def noul(instructions = nil, criteria: nil)
|
|
14
|
-
{ type: "noul", instructions: instructions, criteria: criteria }
|
|
16
|
+
question = { type: "noul", instructions: instructions, criteria: criteria }
|
|
17
|
+
validate_noul!(instructions, criteria, "noul")
|
|
18
|
+
question
|
|
15
19
|
end
|
|
16
20
|
|
|
17
|
-
# Create a score question
|
|
18
|
-
#
|
|
19
|
-
# @param instructions [String, Hash, Array, nil] the question.
|
|
20
|
-
# @param criteria [Array] at least two descriptions indexed by score from zero.
|
|
21
|
+
# Create a score question with 2–10 ordered descriptions. Nil levels are not supported.
|
|
22
|
+
# An empty string preserves a position without a description.
|
|
21
23
|
def score(instructions, criteria)
|
|
22
|
-
|
|
23
|
-
raise TypeSafeError,
|
|
24
|
-
"Score criteria must be a list of descriptions indexed by score from zero, " \
|
|
25
|
-
"not a map."
|
|
26
|
-
end
|
|
27
|
-
|
|
24
|
+
validate_score!(criteria, "score")
|
|
28
25
|
{ type: "score", instructions: instructions, criteria: criteria }
|
|
29
26
|
end
|
|
30
27
|
|
|
31
|
-
# Create a
|
|
32
|
-
#
|
|
33
|
-
# @param instructions [String, Hash, Array, nil] the question.
|
|
34
|
-
# @param criteria [Hash] labels mapped to descriptions, or `nil` for undescribed labels.
|
|
28
|
+
# Create a choice between 1–255 named options; nil leaves a label undescribed.
|
|
35
29
|
def choice(instructions, criteria)
|
|
36
|
-
|
|
37
|
-
raise TypeSafeError, "Choice criteria must be a map of labels to descriptions, not a list."
|
|
38
|
-
end
|
|
39
|
-
|
|
30
|
+
validate_choice!(criteria, "choice")
|
|
40
31
|
{ type: "choice", instructions: instructions, criteria: criteria }
|
|
41
32
|
end
|
|
42
33
|
|
|
43
|
-
#
|
|
34
|
+
# Create a question that locates what its instructions describe in the request's image.
|
|
35
|
+
# Use the `jev-preview` model for box questions.
|
|
36
|
+
# @param instructions [String, Hash, Array, Image, TextWithAssets, nil] what to locate.
|
|
37
|
+
def box(instructions = nil)
|
|
38
|
+
{ type: "box", instructions: instructions }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
STATE_TYPES = [String, Hash, Array, Image, TextWithAssets].freeze
|
|
42
|
+
|
|
43
|
+
def validate_state!(state)
|
|
44
|
+
return if STATE_TYPES.any? { |type| state.is_a?(type) }
|
|
45
|
+
|
|
46
|
+
raise TypeSafeError, "`state` must be a string, object, array, or image; nil is not supported."
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Validate builders and raw JSON-style questions without altering the caller's data.
|
|
44
50
|
def validate!(questions)
|
|
45
|
-
raise TypeSafeError, "At least one question is required."
|
|
51
|
+
raise TypeSafeError, "At least one question is required." unless questions.is_a?(Hash) && !questions.empty?
|
|
46
52
|
|
|
47
53
|
questions.each do |name, question|
|
|
48
|
-
|
|
54
|
+
raise TypeSafeError, "Question key cannot be empty." if name.to_s.empty?
|
|
55
|
+
raise TypeSafeError, "Question must be an object." unless question.is_a?(Hash)
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
wire = question.transform_keys(&:to_s)
|
|
58
|
+
case wire["type"].to_s
|
|
59
|
+
when "score" then validate_score!(wire["criteria"], name)
|
|
60
|
+
when "choice" then validate_choice!(wire["criteria"], name)
|
|
61
|
+
when "noul" then validate_noul!(wire["instructions"], wire["criteria"], name)
|
|
55
62
|
end
|
|
56
|
-
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def validate_noul!(instructions, criteria, name)
|
|
67
|
+
unless criteria.nil? || criteria.is_a?(Hash)
|
|
68
|
+
raise TypeSafeError, "Noul question #{name.inspect} criteria must be an object."
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
descriptions = criteria.is_a?(Hash) ? criteria.transform_keys(&:to_s) : {}
|
|
72
|
+
return unless instructions.nil? && %w[true false].all? { |key| descriptions[key].nil? }
|
|
57
73
|
|
|
74
|
+
raise TypeSafeError, "Noul question #{name.inspect} must have instructions or criteria."
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def validate_score!(criteria, name)
|
|
78
|
+
unless criteria.is_a?(Array)
|
|
58
79
|
raise TypeSafeError,
|
|
59
|
-
"Score question
|
|
60
|
-
"
|
|
80
|
+
"Score question #{name.inspect} has criteria that are not a list; " \
|
|
81
|
+
"score criteria must be a list of descriptions indexed by score from zero."
|
|
61
82
|
end
|
|
83
|
+
raise TypeSafeError, "Score question #{name.inspect}: at least two scores are required." if criteria.length < 2
|
|
84
|
+
if criteria.length > MAX_SCORE_LEVELS
|
|
85
|
+
raise TypeSafeError, "Score question #{name.inspect}: at most #{MAX_SCORE_LEVELS} levels are allowed."
|
|
86
|
+
end
|
|
87
|
+
return unless criteria.any?(&:nil?)
|
|
88
|
+
|
|
89
|
+
raise TypeSafeError,
|
|
90
|
+
"Score question #{name.inspect} contains a nil level; use a description or an empty string " \
|
|
91
|
+
"to preserve its position."
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate_choice!(criteria, name)
|
|
95
|
+
unless criteria.is_a?(Hash)
|
|
96
|
+
raise TypeSafeError, "Choice criteria must be a map of labels to descriptions, not a list."
|
|
97
|
+
end
|
|
98
|
+
return if criteria.length.between?(1, MAX_CHOICE_OPTIONS)
|
|
99
|
+
|
|
100
|
+
raise TypeSafeError, "Choice question #{name.inspect} requires 1–#{MAX_CHOICE_OPTIONS} options."
|
|
62
101
|
end
|
|
63
102
|
end
|
|
64
103
|
end
|
|
@@ -11,11 +11,12 @@ module Typesafe
|
|
|
11
11
|
|
|
12
12
|
# List the models available to the account.
|
|
13
13
|
#
|
|
14
|
-
# @param options [Hash] per-call `timeout`, `
|
|
15
|
-
# @
|
|
14
|
+
# @param options [Hash] per-call `timeout`, `retry_policy`, `headers`, and `signal` settings.
|
|
15
|
+
# @param with_response [Boolean] return model cards with HTTP metadata and request ID.
|
|
16
|
+
# @return [Array<ModelCard>, WithResponse]
|
|
16
17
|
# @raise [TypeSafeError] the response shape is unexpected.
|
|
17
18
|
# @raise [APIError] the server returns a non-2xx response after retries.
|
|
18
|
-
def list(**options)
|
|
19
|
+
def list(with_response: false, **options)
|
|
19
20
|
response = @client.request(:get, "/v1/models", **options)
|
|
20
21
|
wire = response.body
|
|
21
22
|
unless wire.is_a?(Hash) && wire["models"].is_a?(Array)
|
|
@@ -23,7 +24,8 @@ module Typesafe
|
|
|
23
24
|
"Unexpected response shape from GET /v1/models; expected { models: [...] }."
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
wire["models"].map { |card| ModelCard.new(card) }
|
|
27
|
+
result = wire["models"].map { |card| ModelCard.new(card) }
|
|
28
|
+
with_response ? WithResponse.new(result, response) : result
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
end
|
data/lib/typesafe/sdk/retry.rb
CHANGED
|
@@ -38,15 +38,15 @@ module Typesafe
|
|
|
38
38
|
return nil unless headers.respond_to?(:[])
|
|
39
39
|
|
|
40
40
|
if (ms = headers["retry-after-ms"])
|
|
41
|
-
parsed =
|
|
42
|
-
return parsed if parsed && parsed >= 0
|
|
41
|
+
parsed = Float(ms, exception: false) unless ms.to_s.strip.empty?
|
|
42
|
+
return parsed if parsed&.finite? && parsed >= 0
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
raw = headers["retry-after"]
|
|
46
|
-
return nil if raw.nil?
|
|
46
|
+
return nil if raw.nil? || raw.to_s.strip.empty?
|
|
47
47
|
|
|
48
48
|
seconds = Float(raw, exception: false)
|
|
49
|
-
return
|
|
49
|
+
return seconds >= 0 ? seconds * 1000 : nil if seconds&.finite?
|
|
50
50
|
|
|
51
51
|
date = begin
|
|
52
52
|
Time.parse(raw)
|
data/lib/typesafe/sdk/types.rb
CHANGED
|
@@ -2,7 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
module Typesafe
|
|
4
4
|
module SDK
|
|
5
|
-
#
|
|
5
|
+
# Parsed data together with HTTP metadata, like the JS SDK withResponse() result.
|
|
6
|
+
class WithResponse
|
|
7
|
+
attr_reader :data, :response, :request_id
|
|
8
|
+
|
|
9
|
+
def initialize(data, response)
|
|
10
|
+
@data = data
|
|
11
|
+
@response = response
|
|
12
|
+
@request_id = response.request_id
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# An answer, keeping every field the API returned, including fields not modeled here.
|
|
17
|
+
class Answer
|
|
18
|
+
attr_reader :type
|
|
19
|
+
|
|
20
|
+
def initialize(data)
|
|
21
|
+
@data = data
|
|
22
|
+
@type = data["type"]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The answer as the API returned it, with string keys.
|
|
26
|
+
def to_h
|
|
27
|
+
@data.dup
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Token usage for a request. Each count is `nil` when the API did not report it.
|
|
6
32
|
class Usage
|
|
7
33
|
attr_reader :input_tokens, :output_tokens
|
|
8
34
|
|
|
@@ -13,21 +39,21 @@ module Typesafe
|
|
|
13
39
|
end
|
|
14
40
|
|
|
15
41
|
# A yes/no answer.
|
|
16
|
-
class NoulResponse
|
|
17
|
-
attr_reader :
|
|
42
|
+
class NoulResponse < Answer
|
|
43
|
+
attr_reader :noul
|
|
18
44
|
|
|
19
45
|
def initialize(data)
|
|
20
|
-
|
|
46
|
+
super
|
|
21
47
|
@noul = data["noul"]
|
|
22
48
|
end
|
|
23
49
|
end
|
|
24
50
|
|
|
25
51
|
# A selected label with its probabilities.
|
|
26
|
-
class ChoiceResponse
|
|
27
|
-
attr_reader :
|
|
52
|
+
class ChoiceResponse < Answer
|
|
53
|
+
attr_reader :choice, :confidence, :probabilities
|
|
28
54
|
|
|
29
55
|
def initialize(data)
|
|
30
|
-
|
|
56
|
+
super
|
|
31
57
|
@choice = data["choice"]
|
|
32
58
|
@confidence = data["confidence"]
|
|
33
59
|
@probabilities = data["probabilities"]
|
|
@@ -35,11 +61,11 @@ module Typesafe
|
|
|
35
61
|
end
|
|
36
62
|
|
|
37
63
|
# An expected score with its rubric and probabilities.
|
|
38
|
-
class ScoreResponse
|
|
39
|
-
attr_reader :
|
|
64
|
+
class ScoreResponse < Answer
|
|
65
|
+
attr_reader :score, :confidence, :legend, :probabilities
|
|
40
66
|
|
|
41
67
|
def initialize(data)
|
|
42
|
-
|
|
68
|
+
super
|
|
43
69
|
@score = data["score"]
|
|
44
70
|
@confidence = data["confidence"]
|
|
45
71
|
@legend = data["legend"]
|
|
@@ -47,12 +73,44 @@ module Typesafe
|
|
|
47
73
|
end
|
|
48
74
|
end
|
|
49
75
|
|
|
76
|
+
# A box's edges as fractions of the image's height (`top`, `bottom`) and width
|
|
77
|
+
# (`left`, `right`), each from zero to one.
|
|
78
|
+
class BoxCoordinates
|
|
79
|
+
attr_reader :top, :bottom, :left, :right
|
|
80
|
+
|
|
81
|
+
def initialize(data)
|
|
82
|
+
@top = data["top"]
|
|
83
|
+
@bottom = data["bottom"]
|
|
84
|
+
@left = data["left"]
|
|
85
|
+
@right = data["right"]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def to_h
|
|
89
|
+
{ "top" => top, "bottom" => bottom, "left" => left, "right" => right }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The answer to a box question.
|
|
94
|
+
class BoxResponse < Answer
|
|
95
|
+
# Where the instructions' subject is in the image, or `nil` when absent.
|
|
96
|
+
attr_reader :box
|
|
97
|
+
# Confidence in the box, from zero to one, or `nil` when the API didn't report it.
|
|
98
|
+
attr_reader :confidence
|
|
99
|
+
|
|
100
|
+
def initialize(data)
|
|
101
|
+
super
|
|
102
|
+
@box = data["box"].is_a?(Hash) ? BoxCoordinates.new(data["box"]) : nil
|
|
103
|
+
@confidence = data["confidence"]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
50
107
|
# Builds the answer object matching the question type.
|
|
51
108
|
module ResponseFactory
|
|
52
109
|
CLASSES = {
|
|
53
110
|
"noul" => NoulResponse,
|
|
54
111
|
"choice" => ChoiceResponse,
|
|
55
|
-
"score" => ScoreResponse
|
|
112
|
+
"score" => ScoreResponse,
|
|
113
|
+
"box" => BoxResponse
|
|
56
114
|
}.freeze
|
|
57
115
|
|
|
58
116
|
module_function
|
data/lib/typesafe/sdk/version.rb
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Typesafe
|
|
4
4
|
module SDK
|
|
5
|
-
#
|
|
6
|
-
VERSION = "0.
|
|
5
|
+
# Ruby-only fixes use a fourth numeric component; the first three track upstream.
|
|
6
|
+
VERSION = "0.7.0"
|
|
7
|
+
UPSTREAM_VERSION = "0.7.0"
|
|
7
8
|
end
|
|
8
9
|
end
|
data/lib/typesafe-sdk-ruby.rb
CHANGED
|
@@ -5,6 +5,8 @@ require_relative "typesafe/sdk/env"
|
|
|
5
5
|
require_relative "typesafe/sdk/logging"
|
|
6
6
|
require_relative "typesafe/sdk/retry"
|
|
7
7
|
require_relative "typesafe/sdk/errors"
|
|
8
|
+
require_relative "typesafe/sdk/assets"
|
|
9
|
+
require_relative "typesafe/sdk/compression"
|
|
8
10
|
require_relative "typesafe/sdk/questions"
|
|
9
11
|
require_relative "typesafe/sdk/http"
|
|
10
12
|
require_relative "typesafe/sdk/types"
|
|
@@ -36,5 +38,33 @@ module Typesafe
|
|
|
36
38
|
def choice(instructions, criteria)
|
|
37
39
|
Questions.choice(instructions, criteria)
|
|
38
40
|
end
|
|
41
|
+
|
|
42
|
+
# Create a box question. @see Questions.box
|
|
43
|
+
def box(instructions = nil)
|
|
44
|
+
Questions.box(instructions)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Create a PNG or JPEG image asset from file bytes, a Pathname, or an IO.
|
|
48
|
+
# Use {Image.from_base64} for base64 text. @see Image#initialize
|
|
49
|
+
def image(data)
|
|
50
|
+
Image.new(data)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Join text and images into text that carries its images, like JavaScript's `withAssets`
|
|
54
|
+
# tagged template. Other values are converted with `to_s`, as string interpolation does.
|
|
55
|
+
#
|
|
56
|
+
# @example
|
|
57
|
+
# Typesafe::SDK.with_assets("Does ", photo, " show the product in ", listing, "?")
|
|
58
|
+
# @return [TextWithAssets]
|
|
59
|
+
def with_assets(*parts)
|
|
60
|
+
TextWithAssets.build(*parts)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Join `values` with `separator` (default `","`, as in JavaScript's `withAssets.join`) into
|
|
64
|
+
# text that carries the images among them.
|
|
65
|
+
# @return [TextWithAssets]
|
|
66
|
+
def join_assets(values, separator = ",")
|
|
67
|
+
TextWithAssets.join(values, separator)
|
|
68
|
+
end
|
|
39
69
|
end
|
|
40
70
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: typesafe-sdk-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- TypeSafe Ruby SDK Contributors
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: json
|
|
@@ -17,6 +17,9 @@ dependencies:
|
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
19
|
version: '2.7'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '3'
|
|
20
23
|
type: :runtime
|
|
21
24
|
prerelease: false
|
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -24,12 +27,15 @@ dependencies:
|
|
|
24
27
|
- - ">="
|
|
25
28
|
- !ruby/object:Gem::Version
|
|
26
29
|
version: '2.7'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3'
|
|
27
33
|
description: |-
|
|
28
34
|
Unofficial Ruby SDK for the TypeSafe AI API (Jev model). Ask named questions about
|
|
29
|
-
text
|
|
30
|
-
questions, with retries, timeouts, structured logging, and
|
|
35
|
+
text, structured state, or images and get typed answers: yes/no (noul), choice, score,
|
|
36
|
+
and box questions, with retries, timeouts, request compression, structured logging, and
|
|
37
|
+
typed errors.
|
|
31
38
|
email:
|
|
32
|
-
- opensource@typesafe.ai
|
|
33
39
|
executables: []
|
|
34
40
|
extensions: []
|
|
35
41
|
extra_rdoc_files: []
|
|
@@ -37,8 +43,13 @@ files:
|
|
|
37
43
|
- CHANGELOG.md
|
|
38
44
|
- LICENSE
|
|
39
45
|
- README.md
|
|
46
|
+
- docs/COMPATIBILITY.md
|
|
47
|
+
- docs/RELEASING.md
|
|
48
|
+
- docs/UPSTREAM_ISSUES.md
|
|
40
49
|
- lib/typesafe-sdk-ruby.rb
|
|
50
|
+
- lib/typesafe/sdk/assets.rb
|
|
41
51
|
- lib/typesafe/sdk/client.rb
|
|
52
|
+
- lib/typesafe/sdk/compression.rb
|
|
42
53
|
- lib/typesafe/sdk/env.rb
|
|
43
54
|
- lib/typesafe/sdk/errors.rb
|
|
44
55
|
- lib/typesafe/sdk/http.rb
|
|
@@ -54,8 +65,7 @@ licenses:
|
|
|
54
65
|
metadata:
|
|
55
66
|
bug_tracker_uri: https://github.com/afurm/typesafe-sdk-ruby/issues
|
|
56
67
|
changelog_uri: https://github.com/afurm/typesafe-sdk-ruby/blob/main/CHANGELOG.md
|
|
57
|
-
documentation_uri: https://
|
|
58
|
-
homepage_uri: https://github.com/afurm/typesafe-sdk-ruby
|
|
68
|
+
documentation_uri: https://github.com/afurm/typesafe-sdk-ruby#readme
|
|
59
69
|
rubygems_mfa_required: 'true'
|
|
60
70
|
source_code_uri: https://github.com/afurm/typesafe-sdk-ruby
|
|
61
71
|
post_install_message:
|