typesafe-ai-ruby 0.1.0 → 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.
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypeSafe
4
+ module Questions
5
+ # Locates what its instructions describe in an image. The answer is a bounding box.
6
+ # Box questions need an image in the request, the :images danger zone, and Jev 1.14
7
+ # (model "jev-preview" or "jev-1.14.0"). Box takes no criteria.
8
+ #
9
+ # For example:
10
+ # TypeSafe::Box.new(instructions: "the coffee mug in `photo`")
11
+ class Box < Question
12
+ TYPE = "box"
13
+
14
+ def initialize(instructions: nil)
15
+ super(TYPE, instructions: instructions, criteria: nil)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -14,10 +14,13 @@ module TypeSafe
14
14
  TYPE = "choice"
15
15
 
16
16
  def initialize(criteria:, instructions: nil)
17
- super(TYPE, instructions: instructions, criteria: self.class.normalize_criteria(criteria))
17
+ assets = []
18
+ instructions = Util.deep_stringify(instructions, assets: assets) # collect instruction images first
19
+ super(TYPE, instructions: instructions, criteria: self.class.normalize_criteria(criteria, assets: assets),
20
+ assets: assets)
18
21
  end
19
22
 
20
- def self.normalize_criteria(criteria)
23
+ def self.normalize_criteria(criteria, assets: nil)
21
24
  raise ValidationError, "Choice criteria must be a non-empty Hash of option => description" unless
22
25
  criteria.is_a?(Hash) && !criteria.empty?
23
26
 
@@ -25,7 +28,7 @@ module TypeSafe
25
28
  name = Util.stringify_key(key)
26
29
  raise ValidationError, "Choice option names must not be blank" if name.strip.empty?
27
30
 
28
- result[name] = value.nil? ? nil : Util.deep_stringify(value)
31
+ result[name] = value.nil? ? nil : Util.deep_stringify(value, assets: assets)
29
32
  end
30
33
  end
31
34
  end
@@ -6,36 +6,39 @@ module TypeSafe
6
6
  # mistakes before a request is sent.
7
7
  module Normalizer
8
8
  TYPES = {
9
- Noul::TYPE => Noul, Choice::TYPE => Choice, Score::TYPE => Score
9
+ Noul::TYPE => Noul, Choice::TYPE => Choice, Score::TYPE => Score, Box::TYPE => Box
10
10
  }.freeze
11
11
 
12
12
  module_function
13
13
 
14
- def state(state)
14
+ # Images found in the state are appended to +assets+ and replaced by their references.
15
+ def state(state, assets: nil)
15
16
  raise ValidationError, "state must not be nil; pass a String, Hash, or Array" if state.nil?
16
17
 
17
- value = Util.deep_stringify(state)
18
+ value = Util.deep_stringify(state, assets: assets)
18
19
  return value if value.is_a?(String) || value.is_a?(Hash) || value.is_a?(Array)
19
20
 
20
21
  raise ValidationError, "state must be a String, Hash, or Array, got #{state.class}"
21
22
  end
22
23
 
23
24
  # Questions in wire format.
24
- def questions(questions)
25
+ def questions(questions, assets: nil)
25
26
  raise ValidationError, "questions must be a non-empty Hash" unless questions.is_a?(Hash) && !questions.empty?
26
27
 
27
28
  questions.each_with_object({}) do |(name, question), result|
28
29
  key = Util.stringify_key(name)
29
30
  raise ValidationError, "question names must not be blank" if key.strip.empty?
30
31
 
31
- result[key] = question(question, key)
32
+ result[key] = question(question, key, assets: assets)
32
33
  end
33
34
  end
34
35
 
35
- def question(question, name)
36
+ def question(question, name, assets: nil)
36
37
  case question
37
- when Question then question.to_h
38
- when Hash then raw_question(question, name)
38
+ when Question
39
+ assets&.concat(question.assets)
40
+ question.to_h
41
+ when Hash then raw_question(question, name, assets: assets)
39
42
  else
40
43
  raise ValidationError,
41
44
  "question #{name.inspect} must be a TypeSafe::Noul, Choice, Score, or a Hash with a \"type\" key"
@@ -44,8 +47,8 @@ module TypeSafe
44
47
 
45
48
  # Validate a raw Hash question. Known types get the same checks as the typed classes;
46
49
  # unknown types and extra keys pass through untouched so new API fields work immediately.
47
- def raw_question(hash, name)
48
- wire = Util.deep_stringify(hash)
50
+ def raw_question(hash, name, assets: nil)
51
+ wire = Util.deep_stringify(hash, assets: assets)
49
52
  type = wire["type"]
50
53
  unless type.is_a?(String) && !type.empty?
51
54
  raise ValidationError,
@@ -53,7 +56,11 @@ module TypeSafe
53
56
  end
54
57
 
55
58
  klass = TYPES[type]
56
- wire["criteria"] = klass.normalize_criteria(wire["criteria"]) if klass && requires_criteria?(klass, wire)
59
+ if klass == Box
60
+ raise ValidationError, "question #{name.inspect}: box questions take no criteria" if wire.key?("criteria")
61
+ elsif klass && requires_criteria?(klass, wire)
62
+ wire["criteria"] = klass.normalize_criteria(wire["criteria"])
63
+ end
57
64
  wire
58
65
  end
59
66
 
@@ -14,10 +14,13 @@ module TypeSafe
14
14
  KEYS = %w[true false].freeze
15
15
 
16
16
  def initialize(instructions: nil, criteria: nil)
17
- super(TYPE, instructions: instructions, criteria: self.class.normalize_criteria(criteria))
17
+ assets = []
18
+ instructions = Util.deep_stringify(instructions, assets: assets) # collect instruction images first
19
+ super(TYPE, instructions: instructions, criteria: self.class.normalize_criteria(criteria, assets: assets),
20
+ assets: assets)
18
21
  end
19
22
 
20
- def self.normalize_criteria(criteria)
23
+ def self.normalize_criteria(criteria, assets: nil)
21
24
  return nil if criteria.nil?
22
25
  raise ValidationError, "Noul criteria must be a Hash with true/false keys" unless criteria.is_a?(Hash)
23
26
 
@@ -28,7 +31,7 @@ module TypeSafe
28
31
  "Noul criteria keys must be true or false, got #{key.inspect}"
29
32
  end
30
33
 
31
- result[name] = value.nil? ? nil : Util.deep_stringify(value)
34
+ result[name] = value.nil? ? nil : Util.deep_stringify(value, assets: assets)
32
35
  end
33
36
  end
34
37
  end
@@ -11,11 +11,16 @@ module TypeSafe
11
11
  attr_reader :instructions
12
12
  # Type-specific rubric.
13
13
  attr_reader :criteria
14
+ # Images placed in the instructions or criteria, sent as assets with the request.
15
+ attr_reader :assets
14
16
 
15
- def initialize(type, instructions:, criteria:)
17
+ def initialize(type, instructions:, criteria:, assets: [])
16
18
  @type = type
17
- @instructions = instructions.nil? ? nil : Util.deep_stringify(instructions)
19
+ @assets = assets
20
+ @instructions = instructions.nil? ? nil : Util.deep_stringify(instructions, assets: @assets)
18
21
  @criteria = criteria
22
+ @assets.uniq!
23
+ @assets.freeze
19
24
  freeze
20
25
  end
21
26
 
@@ -14,17 +14,20 @@ module TypeSafe
14
14
  MIN_LEVELS = 2
15
15
 
16
16
  def initialize(criteria:, instructions: nil)
17
- super(TYPE, instructions: instructions, criteria: self.class.normalize_criteria(criteria))
17
+ assets = []
18
+ instructions = Util.deep_stringify(instructions, assets: assets) # collect instruction images first
19
+ super(TYPE, instructions: instructions, criteria: self.class.normalize_criteria(criteria, assets: assets),
20
+ assets: assets)
18
21
  end
19
22
 
20
- def self.normalize_criteria(criteria)
23
+ def self.normalize_criteria(criteria, assets: nil)
21
24
  raise ValidationError, "Score criteria must be an Array of at least #{MIN_LEVELS} levels" unless
22
25
  criteria.is_a?(Array) && criteria.length >= MIN_LEVELS
23
26
 
24
27
  criteria.map do |level|
25
28
  raise ValidationError, "Score levels must not be nil" if level.nil?
26
29
 
27
- Util.deep_stringify(level)
30
+ Util.deep_stringify(level, assets: assets)
28
31
  end
29
32
  end
30
33
  end
@@ -3,7 +3,7 @@
3
3
  module TypeSafe
4
4
  # Per-call overrides accepted by Client#system_one and Resources::Models#list.
5
5
  class RequestOptions
6
- KEYS = %i[timeout headers extra_body retry_policy].freeze
6
+ KEYS = %i[timeout headers extra_body retry_policy danger_zone].freeze
7
7
 
8
8
  # Timeout per attempt in seconds for this call only.
9
9
  attr_reader :timeout
@@ -13,21 +13,32 @@ module TypeSafe
13
13
  attr_reader :extra_body
14
14
  # Retry policy override for this call only: a RetryPolicy, or a Hash applied on the client policy.
15
15
  attr_reader :retry_policy
16
+ # Experimental features to turn on for this call: :images and/or :long_context.
17
+ attr_reader :danger_zone
16
18
 
17
- def self.from(options)
18
- case options
19
- when RequestOptions then options
20
- when nil then new
21
- when Hash then new(**options.transform_keys(&:to_sym))
22
- else raise ArgumentError, "request_options must be a Hash, got #{options.class}"
23
- end
19
+ # Coerce nil, a Hash, or a RequestOptions. A danger_zone given separately is merged in.
20
+ def self.from(options, danger_zone: nil)
21
+ result = case options
22
+ when RequestOptions then options
23
+ when nil then new
24
+ when Hash then new(**options.transform_keys(&:to_sym))
25
+ else raise ArgumentError, "request_options must be a Hash, got #{options.class}"
26
+ end
27
+ danger_zone.nil? ? result : result.with_danger_zone(danger_zone)
24
28
  end
25
29
 
26
- def initialize(timeout: nil, headers: nil, extra_body: nil, retry_policy: nil)
30
+ # A copy with additional danger zones turned on.
31
+ def with_danger_zone(zones)
32
+ self.class.new(timeout: timeout, headers: headers, extra_body: extra_body, retry_policy: retry_policy,
33
+ danger_zone: danger_zone + Util.danger_zones(zones))
34
+ end
35
+
36
+ def initialize(timeout: nil, headers: nil, extra_body: nil, retry_policy: nil, danger_zone: nil)
27
37
  @timeout = validate_timeout(timeout)
28
38
  @headers = (headers || {}).each_with_object({}) { |(name, value), result| result[name.to_s] = value.to_s }
29
39
  @extra_body = validate_extra_body(extra_body)
30
40
  @retry_policy = validate_retry_policy(retry_policy)
41
+ @danger_zone = Util.danger_zones(danger_zone).freeze
31
42
  freeze
32
43
  end
33
44
 
@@ -129,6 +129,79 @@ module TypeSafe
129
129
  end
130
130
  end
131
131
 
132
+ # The four edges of a box as fractions of the image's width (left, right) and height
133
+ # (top, bottom), measured from the top-left corner.
134
+ class BoxCoordinates
135
+ attr_reader :top, :bottom, :left, :right
136
+
137
+ def self.parse(data, path)
138
+ box = Reader.hash!(data, path)
139
+ new(**%i[top bottom left right].to_h { |edge| [edge, Reader.number!(box[edge.to_s], "#{path}.#{edge}")] })
140
+ end
141
+
142
+ def initialize(top:, bottom:, left:, right:)
143
+ @top = top
144
+ @bottom = bottom
145
+ @left = left
146
+ @right = right
147
+ freeze
148
+ end
149
+
150
+ def width
151
+ right - left
152
+ end
153
+
154
+ def height
155
+ bottom - top
156
+ end
157
+
158
+ # [left, top, right, bottom], the order most image libraries use for a crop box.
159
+ def to_a
160
+ [left, top, right, bottom]
161
+ end
162
+
163
+ # [left, top, right, bottom] in integer pixels for an image of the given size.
164
+ def to_pixels(width:, height:)
165
+ [(left * width).round, (top * height).round, (right * width).round, (bottom * height).round]
166
+ end
167
+
168
+ def to_h
169
+ { "top" => top, "bottom" => bottom, "left" => left, "right" => right }
170
+ end
171
+
172
+ def ==(other)
173
+ other.is_a?(BoxCoordinates) && to_a == other.to_a
174
+ end
175
+ alias eql? ==
176
+
177
+ def hash
178
+ to_a.hash
179
+ end
180
+ end
181
+
182
+ # A located box with its confidence.
183
+ class BoxAnswer < Answer
184
+ attr_reader :box
185
+ # Confidence from 0 to 1, or nil when the API did not report it.
186
+ attr_reader :confidence
187
+
188
+ def self.parse(data, path)
189
+ confidence = data["confidence"]
190
+ new(
191
+ data,
192
+ box: BoxCoordinates.parse(data["box"], "#{path}.box"),
193
+ confidence: confidence.nil? ? nil : Reader.number!(confidence, "#{path}.confidence")
194
+ )
195
+ end
196
+
197
+ def initialize(raw, box:, confidence:)
198
+ @box = box
199
+ @confidence = confidence
200
+ super(raw)
201
+ freeze
202
+ end
203
+ end
204
+
132
205
  # An answer whose type this gem does not recognize. Only #raw is available.
133
206
  class UnknownAnswer < Answer
134
207
  def self.parse(data, _path)
@@ -137,7 +210,7 @@ module TypeSafe
137
210
  end
138
211
 
139
212
  Answer::TYPES = {
140
- "noul" => NoulAnswer, "choice" => ChoiceAnswer, "score" => ScoreAnswer
213
+ "noul" => NoulAnswer, "choice" => ChoiceAnswer, "score" => ScoreAnswer, "box" => BoxAnswer
141
214
  }.freeze
142
215
  end
143
216
  end
@@ -71,6 +71,10 @@ module TypeSafe
71
71
  answers_of(ScoreAnswer)
72
72
  end
73
73
 
74
+ def boxes
75
+ answers_of(BoxAnswer)
76
+ end
77
+
74
78
  def to_h
75
79
  raw || { "model" => model, "usage" => usage.to_h, "answers" => answers.transform_values(&:to_h) }
76
80
  end
data/lib/typesafe/util.rb CHANGED
@@ -30,21 +30,30 @@ module TypeSafe
30
30
  end
31
31
 
32
32
  # Convert a value into plain JSON-compatible Ruby: string keys, no symbols, finite floats.
33
- # Objects responding to as_json are converted through it. Anything else raises.
34
- def deep_stringify(value)
33
+ # Objects responding to as_json are converted through it. Images become their reference and
34
+ # are appended to +assets+; without an +assets+ collector an Image raises. Anything else raises.
35
+ def deep_stringify(value, assets: nil)
35
36
  case value
36
37
  when String, Integer, true, false, nil then value
37
38
  when Symbol then value.to_s
38
39
  when Float then finite_float(value)
39
- when Hash, Array then stringify_container(value)
40
- else convert_object(value)
40
+ when Image then image_reference(value, assets)
41
+ when Hash, Array then stringify_container(value, assets)
42
+ else convert_object(value, assets)
41
43
  end
42
44
  end
43
45
 
44
- def stringify_container(value)
45
- return value.map { |v| deep_stringify(v) } if value.is_a?(Array)
46
+ def image_reference(image, assets)
47
+ raise ValidationError, "images can only be placed in state, instructions and criteria" if assets.nil?
46
48
 
47
- value.each_with_object({}) { |(k, v), h| h[stringify_key(k)] = deep_stringify(v) }
49
+ assets << image
50
+ image.reference
51
+ end
52
+
53
+ def stringify_container(value, assets)
54
+ return value.map { |v| deep_stringify(v, assets: assets) } if value.is_a?(Array)
55
+
56
+ value.each_with_object({}) { |(k, v), h| h[stringify_key(k)] = deep_stringify(v, assets: assets) }
48
57
  end
49
58
 
50
59
  def finite_float(value)
@@ -61,7 +70,7 @@ module TypeSafe
61
70
  end
62
71
  end
63
72
 
64
- def convert_object(value)
73
+ def convert_object(value, assets)
65
74
  unless value.respond_to?(:as_json)
66
75
  raise ValidationError,
67
76
  "cannot encode #{value.class} as JSON; pass a String, Hash, Array, or an object responding to as_json"
@@ -70,7 +79,7 @@ module TypeSafe
70
79
  converted = value.as_json
71
80
  raise ValidationError, "#{value.class}#as_json returned itself and cannot be encoded" if converted.equal?(value)
72
81
 
73
- deep_stringify(converted)
82
+ deep_stringify(converted, assets: assets)
74
83
  end
75
84
 
76
85
  # Parse retry-after-ms (preferred) or Retry-After (seconds or HTTP-date) into seconds.
@@ -112,6 +121,17 @@ module TypeSafe
112
121
  JSON.generate(body)
113
122
  end
114
123
 
124
+ # Normalize a danger_zone option into an Array of known Symbols.
125
+ def danger_zones(value)
126
+ zones = Array(value).map { |zone| zone.to_s.to_sym }.uniq
127
+ unknown = zones - Constants::DANGER_ZONES.keys
128
+ return zones if unknown.empty?
129
+
130
+ raise ArgumentError,
131
+ "unknown danger_zone #{unknown.map(&:inspect).join(", ")}; " \
132
+ "expected #{Constants::DANGER_ZONES.keys.map(&:inspect).join(", ")}"
133
+ end
134
+
115
135
  # The runtime identifier sent in X-TypeSafe-Runtime.
116
136
  def runtime
117
137
  "ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM})"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TypeSafe
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/typesafe.rb CHANGED
@@ -14,14 +14,18 @@ require_relative "typesafe/logging"
14
14
  require_relative "typesafe/instrumentation"
15
15
  require_relative "typesafe/configuration"
16
16
  require_relative "typesafe/retry_policy"
17
+ require_relative "typesafe/image"
18
+ require_relative "typesafe/assets"
17
19
  require_relative "typesafe/request_options"
18
20
  require_relative "typesafe/questions/question"
19
21
  require_relative "typesafe/questions/noul"
20
22
  require_relative "typesafe/questions/choice"
21
23
  require_relative "typesafe/questions/score"
24
+ require_relative "typesafe/questions/box"
22
25
  require_relative "typesafe/questions/normalizer"
23
26
  require_relative "typesafe/http/request"
24
27
  require_relative "typesafe/http/response"
28
+ require_relative "typesafe/http/multipart_encoder"
25
29
  require_relative "typesafe/http/connection_manager"
26
30
  require_relative "typesafe/http/net_http_transport"
27
31
  require_relative "typesafe/http/retrier"
@@ -41,6 +45,7 @@ module TypeSafe
41
45
  Noul = Questions::Noul
42
46
  Choice = Questions::Choice
43
47
  Score = Questions::Score
48
+ Box = Questions::Box
44
49
 
45
50
  class << self
46
51
  # Global settings used by .client. Mutate through .configure.
@@ -102,6 +107,14 @@ module TypeSafe
102
107
  Score.new(instructions: instructions, criteria: criteria)
103
108
  end
104
109
 
110
+ # Build a box question.
111
+ #
112
+ # For example:
113
+ # TypeSafe.box("the coffee mug in `photo`")
114
+ def box(instructions = nil)
115
+ Box.new(instructions: instructions)
116
+ end
117
+
105
118
  private
106
119
 
107
120
  def merge_criteria(positional, named)
@@ -11,8 +11,10 @@ module TypeSafe
11
11
  attr_reader headers: Hash[String, String]
12
12
  attr_reader body: String?
13
13
  attr_reader timeout: Numeric
14
+ attr_reader body_summary: String?
14
15
 
15
- def initialize: (method: Symbol, uri: URI::HTTP, headers: Hash[String, String], body: String?, timeout: Numeric) -> void
16
+ def initialize: (method: Symbol, uri: URI::HTTP, headers: Hash[String, String], body: String?, timeout: Numeric,
17
+ ?body_summary: String?) -> void
16
18
  def endpoint: () -> String
17
19
  def with_headers: (Hash[String, String] extra) -> Request
18
20
  end
@@ -31,6 +33,29 @@ module TypeSafe
31
33
  def retry_after: () -> Float?
32
34
  end
33
35
 
36
+ class MultipartEncoder
37
+ class Part
38
+ attr_accessor name: String
39
+ attr_accessor content_type: String
40
+ attr_accessor content: String
41
+
42
+ def initialize: (name: String, content_type: String, content: String) -> void
43
+ end
44
+
45
+ class Encoded
46
+ attr_accessor body: String
47
+ attr_accessor content_type: String
48
+ attr_accessor summary: String
49
+ end
50
+
51
+ attr_reader boundary: String
52
+
53
+ def self.build: (Hash[String, untyped] body, Hash[String, Image] assets) -> Encoded
54
+ def initialize: (?boundary: String) -> void
55
+ def content_type: () -> String
56
+ def encode: (String json, Array[Part] parts) -> String
57
+ end
58
+
34
59
  class ConnectionManager
35
60
  IDLE_TIMEOUT: Integer
36
61
 
@@ -69,7 +94,7 @@ module TypeSafe
69
94
 
70
95
  def initialize: (Configuration config, ?transport: _Transport?, ?sleeper: (^(Numeric) -> void)?,
71
96
  ?clock: (^() -> Float)?) -> void
72
- def post: (String path, body: Hash[String, untyped], ?options: RequestOptions) -> Response
97
+ def post: (String path, body: Hash[String, untyped], ?options: RequestOptions, ?assets: Hash[String, Image]) -> Response
73
98
  def get: (String path, ?options: RequestOptions) -> Response
74
99
  def close: () -> void
75
100
  end
@@ -4,8 +4,9 @@ module TypeSafe
4
4
  attr_reader type: String
5
5
  attr_reader instructions: json_content?
6
6
  attr_reader criteria: untyped
7
+ attr_reader assets: Array[Image]
7
8
 
8
- def initialize: (String type, instructions: json_input?, criteria: untyped) -> void
9
+ def initialize: (String type, instructions: json_input?, criteria: untyped, ?assets: Array[Image]) -> void
9
10
  def to_h: () -> Hash[String, untyped]
10
11
  def to_json: (*untyped args) -> String
11
12
  def ==: (untyped other) -> bool
@@ -18,14 +19,14 @@ module TypeSafe
18
19
  KEYS: Array[String]
19
20
 
20
21
  def initialize: (?instructions: json_input?, ?criteria: Hash[untyped, untyped]?) -> void
21
- def self.normalize_criteria: (Hash[untyped, untyped]? criteria) -> Hash[String, json_content?]?
22
+ def self.normalize_criteria: (Hash[untyped, untyped]? criteria, ?assets: Array[Image]?) -> Hash[String, json_content?]?
22
23
  end
23
24
 
24
25
  class Choice < Question
25
26
  TYPE: String
26
27
 
27
28
  def initialize: (?instructions: json_input?, criteria: Hash[untyped, untyped]) -> void
28
- def self.normalize_criteria: (Hash[untyped, untyped] criteria) -> Hash[String, json_content?]
29
+ def self.normalize_criteria: (Hash[untyped, untyped] criteria, ?assets: Array[Image]?) -> Hash[String, json_content?]
29
30
  end
30
31
 
31
32
  class Score < Question
@@ -33,16 +34,22 @@ module TypeSafe
33
34
  MIN_LEVELS: Integer
34
35
 
35
36
  def initialize: (?instructions: json_input?, criteria: Array[untyped]) -> void
36
- def self.normalize_criteria: (Array[untyped] criteria) -> Array[json_content]
37
+ def self.normalize_criteria: (Array[untyped] criteria, ?assets: Array[Image]?) -> Array[json_content]
38
+ end
39
+
40
+ class Box < Question
41
+ TYPE: String
42
+
43
+ def initialize: (?instructions: json_input?) -> void
37
44
  end
38
45
 
39
46
  module Normalizer
40
47
  TYPES: Hash[String, singleton(Question)]
41
48
 
42
- def self?.state: (json_input state) -> json_content
43
- def self?.questions: (Hash[String | Symbol, question_input] questions) -> Hash[String, Hash[String, untyped]]
44
- def self?.question: (question_input question, String name) -> Hash[String, untyped]
45
- def self?.raw_question: (Hash[untyped, untyped] hash, String name) -> Hash[String, untyped]
49
+ def self?.state: (json_input | Image state, ?assets: Array[Image]?) -> json_content
50
+ def self?.questions: (Hash[String | Symbol, question_input] questions, ?assets: Array[Image]?) -> Hash[String, Hash[String, untyped]]
51
+ def self?.question: (question_input question, String name, ?assets: Array[Image]?) -> Hash[String, untyped]
52
+ def self?.raw_question: (Hash[untyped, untyped] hash, String name, ?assets: Array[Image]?) -> Hash[String, untyped]
46
53
  end
47
54
  end
48
55
  end
@@ -73,6 +73,32 @@ module TypeSafe
73
73
  def raw_probabilities: () -> Hash[String, Numeric]
74
74
  end
75
75
 
76
+ class BoxCoordinates
77
+ attr_reader top: Float
78
+ attr_reader bottom: Float
79
+ attr_reader left: Float
80
+ attr_reader right: Float
81
+
82
+ def self.parse: (untyped data, String path) -> BoxCoordinates
83
+ def initialize: (top: Float, bottom: Float, left: Float, right: Float) -> void
84
+ def width: () -> Float
85
+ def height: () -> Float
86
+ def to_a: () -> [Float, Float, Float, Float]
87
+ def to_pixels: (width: Numeric, height: Numeric) -> [Integer, Integer, Integer, Integer]
88
+ def to_h: () -> Hash[String, Float]
89
+ def ==: (untyped other) -> bool
90
+ alias eql? ==
91
+ def hash: () -> Integer
92
+ end
93
+
94
+ class BoxAnswer < Answer
95
+ attr_reader box: BoxCoordinates
96
+ attr_reader confidence: Float?
97
+
98
+ def self.parse: (Hash[String, untyped] data, String path) -> BoxAnswer
99
+ def initialize: (Hash[String, untyped] raw, box: BoxCoordinates, confidence: Float?) -> void
100
+ end
101
+
76
102
  class UnknownAnswer < Answer
77
103
  def self.parse: (Hash[String, untyped] data, String path) -> UnknownAnswer
78
104
  end
@@ -94,6 +120,7 @@ module TypeSafe
94
120
  def nouls: () -> IndifferentHash
95
121
  def choices: () -> IndifferentHash
96
122
  def scores: () -> IndifferentHash
123
+ def boxes: () -> IndifferentHash
97
124
  def to_h: () -> Hash[String, untyped]
98
125
  def to_json: (*untyped args) -> String
99
126
  end