typesafe-ai-ruby 0.1.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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +21 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +176 -0
  5. data/lib/typesafe/client.rb +56 -0
  6. data/lib/typesafe/configuration.rb +129 -0
  7. data/lib/typesafe/constants.rb +49 -0
  8. data/lib/typesafe/errors.rb +117 -0
  9. data/lib/typesafe/http/connection_manager.rb +82 -0
  10. data/lib/typesafe/http/net_http_transport.rb +74 -0
  11. data/lib/typesafe/http/request.rb +30 -0
  12. data/lib/typesafe/http/requestor.rb +153 -0
  13. data/lib/typesafe/http/response.rb +48 -0
  14. data/lib/typesafe/http/retrier.rb +57 -0
  15. data/lib/typesafe/instrumentation.rb +84 -0
  16. data/lib/typesafe/logging.rb +37 -0
  17. data/lib/typesafe/questions/choice.rb +33 -0
  18. data/lib/typesafe/questions/normalizer.rb +65 -0
  19. data/lib/typesafe/questions/noul.rb +36 -0
  20. data/lib/typesafe/questions/question.rb +48 -0
  21. data/lib/typesafe/questions/score.rb +32 -0
  22. data/lib/typesafe/request_options.rb +56 -0
  23. data/lib/typesafe/resources/models.rb +18 -0
  24. data/lib/typesafe/responses/answer.rb +143 -0
  25. data/lib/typesafe/responses/list_models_response.rb +93 -0
  26. data/lib/typesafe/responses/reader.rb +75 -0
  27. data/lib/typesafe/responses/system_one_response.rb +89 -0
  28. data/lib/typesafe/responses/usage.rb +37 -0
  29. data/lib/typesafe/retry_policy.rb +133 -0
  30. data/lib/typesafe/util.rb +156 -0
  31. data/lib/typesafe/version.rb +5 -0
  32. data/lib/typesafe-ai-ruby.rb +4 -0
  33. data/lib/typesafe.rb +114 -0
  34. data/sig/typesafe/http.rbs +77 -0
  35. data/sig/typesafe/questions.rbs +48 -0
  36. data/sig/typesafe/responses.rbs +131 -0
  37. data/sig/typesafe.rbs +261 -0
  38. metadata +97 -0
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module TypeSafe
6
+ # Internal helpers shared across the gem. Not part of the public API.
7
+ module Util
8
+ RETRY_AFTER_MS_HEADER = "retry-after-ms"
9
+ RETRY_AFTER_HEADER = "retry-after"
10
+ NUMERIC = /\A\d+(?:\.\d+)?\z/
11
+
12
+ module_function
13
+
14
+ # True for nil or whitespace-only strings.
15
+ def blank?(value)
16
+ value.nil? || (value.respond_to?(:strip) && value.strip.empty?)
17
+ end
18
+
19
+ # Read an environment variable, treating blank values as unset.
20
+ def env_value(env, name)
21
+ value = env[name]
22
+ blank?(value) ? nil : value.strip
23
+ end
24
+
25
+ # Headers with lower-cased names and string values.
26
+ def normalize_headers(headers)
27
+ (headers || {}).each_with_object({}) do |(name, value), result|
28
+ result[name.to_s.downcase] = value.is_a?(Array) ? value.join(", ") : value.to_s
29
+ end
30
+ end
31
+
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)
35
+ case value
36
+ when String, Integer, true, false, nil then value
37
+ when Symbol then value.to_s
38
+ when Float then finite_float(value)
39
+ when Hash, Array then stringify_container(value)
40
+ else convert_object(value)
41
+ end
42
+ end
43
+
44
+ def stringify_container(value)
45
+ return value.map { |v| deep_stringify(v) } if value.is_a?(Array)
46
+
47
+ value.each_with_object({}) { |(k, v), h| h[stringify_key(k)] = deep_stringify(v) }
48
+ end
49
+
50
+ def finite_float(value)
51
+ raise ValidationError, "non-finite Float #{value} cannot be encoded as JSON" unless value.finite?
52
+
53
+ value
54
+ end
55
+
56
+ def stringify_key(key)
57
+ case key
58
+ when String then key
59
+ when Symbol, Integer, true, false then key.to_s
60
+ else raise ValidationError, "cannot use #{key.class} as a JSON object key"
61
+ end
62
+ end
63
+
64
+ def convert_object(value)
65
+ unless value.respond_to?(:as_json)
66
+ raise ValidationError,
67
+ "cannot encode #{value.class} as JSON; pass a String, Hash, Array, or an object responding to as_json"
68
+ end
69
+
70
+ converted = value.as_json
71
+ raise ValidationError, "#{value.class}#as_json returned itself and cannot be encoded" if converted.equal?(value)
72
+
73
+ deep_stringify(converted)
74
+ end
75
+
76
+ # Parse retry-after-ms (preferred) or Retry-After (seconds or HTTP-date) into seconds.
77
+ def parse_retry_after(headers)
78
+ normalized = normalize_headers(headers)
79
+ ms = normalized[RETRY_AFTER_MS_HEADER]
80
+ return ms.to_f / 1000.0 if ms&.match?(NUMERIC)
81
+
82
+ raw = normalized[RETRY_AFTER_HEADER]
83
+ return nil if raw.nil?
84
+ return raw.to_f if raw.match?(NUMERIC)
85
+
86
+ delta = Time.httpdate(raw) - Time.now
87
+ delta.positive? ? delta : 0.0
88
+ rescue ArgumentError
89
+ nil
90
+ end
91
+
92
+ # A short human-readable rendering of an error body for exception messages.
93
+ def summarize_body(body, limit: 200)
94
+ text = case body
95
+ when nil then ""
96
+ when Hash then summarize_hash(body)
97
+ when String then body.strip
98
+ else JSON.generate(body)
99
+ end
100
+ text.length > limit ? "#{text[0, limit]}..." : text
101
+ end
102
+
103
+ def summarize_hash(body)
104
+ error = body["error"]
105
+ return error if error.is_a?(String)
106
+ return error["message"] if error.is_a?(Hash) && error["message"].is_a?(String)
107
+
108
+ %w[message detail].each do |key|
109
+ value = body[key]
110
+ return value.is_a?(String) ? value : JSON.generate(value) if value
111
+ end
112
+ JSON.generate(body)
113
+ end
114
+
115
+ # The runtime identifier sent in X-TypeSafe-Runtime.
116
+ def runtime
117
+ "ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM})"
118
+ end
119
+ end
120
+
121
+ # A Hash whose string keys can also be looked up with symbols.
122
+ class IndifferentHash < Hash
123
+ def self.from(hash)
124
+ new.tap { |result| hash.each { |key, value| result[key] = value } }
125
+ end
126
+
127
+ def [](key)
128
+ super(convert(key))
129
+ end
130
+
131
+ def []=(key, value)
132
+ super(convert(key), value)
133
+ end
134
+
135
+ def fetch(key, ...)
136
+ super(convert(key), ...)
137
+ end
138
+
139
+ def key?(key)
140
+ super(convert(key))
141
+ end
142
+ alias has_key? key?
143
+ alias include? key?
144
+ alias member? key?
145
+
146
+ def dig(key, *rest)
147
+ super(convert(key), *rest)
148
+ end
149
+
150
+ private
151
+
152
+ def convert(key)
153
+ key.is_a?(Symbol) ? key.to_s : key
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypeSafe
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Entry point matching the gem name so that Bundler's automatic require works.
4
+ require_relative "typesafe"
data/lib/typesafe.rb ADDED
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "logger"
5
+ require "net/http"
6
+ require "openssl"
7
+ require "uri"
8
+
9
+ require_relative "typesafe/version"
10
+ require_relative "typesafe/constants"
11
+ require_relative "typesafe/util"
12
+ require_relative "typesafe/errors"
13
+ require_relative "typesafe/logging"
14
+ require_relative "typesafe/instrumentation"
15
+ require_relative "typesafe/configuration"
16
+ require_relative "typesafe/retry_policy"
17
+ require_relative "typesafe/request_options"
18
+ require_relative "typesafe/questions/question"
19
+ require_relative "typesafe/questions/noul"
20
+ require_relative "typesafe/questions/choice"
21
+ require_relative "typesafe/questions/score"
22
+ require_relative "typesafe/questions/normalizer"
23
+ require_relative "typesafe/http/request"
24
+ require_relative "typesafe/http/response"
25
+ require_relative "typesafe/http/connection_manager"
26
+ require_relative "typesafe/http/net_http_transport"
27
+ require_relative "typesafe/http/retrier"
28
+ require_relative "typesafe/http/requestor"
29
+ require_relative "typesafe/responses/reader"
30
+ require_relative "typesafe/responses/usage"
31
+ require_relative "typesafe/responses/answer"
32
+ require_relative "typesafe/responses/system_one_response"
33
+ require_relative "typesafe/responses/list_models_response"
34
+ require_relative "typesafe/resources/models"
35
+ require_relative "typesafe/client"
36
+
37
+ # Ruby client for the TypeSafe AI System One API.
38
+ #
39
+ # See https://docs.typesafe.ai/ for the HTTP API this gem wraps.
40
+ module TypeSafe
41
+ Noul = Questions::Noul
42
+ Choice = Questions::Choice
43
+ Score = Questions::Score
44
+
45
+ class << self
46
+ # Global settings used by .client. Mutate through .configure.
47
+ def configuration
48
+ @configuration ||= Configuration.new
49
+ end
50
+
51
+ # Configure the default client.
52
+ #
53
+ # For example:
54
+ # TypeSafe.configure do |c|
55
+ # c.api_key = Rails.application.credentials.typesafe_api_key
56
+ # c.logger = Rails.logger
57
+ # end
58
+ def configure
59
+ yield configuration
60
+ reset_client!
61
+ configuration
62
+ end
63
+
64
+ # The default client built from .configuration. Memoized until .configure is called again.
65
+ def client
66
+ @client ||= Client.new(**configuration.to_h)
67
+ end
68
+
69
+ # Discard the memoized default client. Mostly useful in tests.
70
+ def reset_client!
71
+ @client&.close
72
+ @client = nil
73
+ end
74
+
75
+ # Reset global configuration and the default client. Mostly useful in tests.
76
+ def reset!
77
+ reset_client!
78
+ @configuration = nil
79
+ end
80
+
81
+ # Build a yes/no question.
82
+ #
83
+ # For example:
84
+ # TypeSafe.noul("Is this spam?", true: "Unsolicited advertising", false: "A real conversation")
85
+ def noul(instructions = nil, criteria = nil, **named)
86
+ Noul.new(instructions: instructions, criteria: merge_criteria(criteria, named))
87
+ end
88
+
89
+ # Build a choice question.
90
+ #
91
+ # For example:
92
+ # TypeSafe.choice("What is the tone?", calm: nil, angry: "Shouting or threats")
93
+ def choice(instructions = nil, criteria = nil, **named)
94
+ Choice.new(instructions: instructions, criteria: merge_criteria(criteria, named) || {})
95
+ end
96
+
97
+ # Build a score question.
98
+ #
99
+ # For example:
100
+ # TypeSafe.score("How urgent is this?", ["can wait", "this week", "today"])
101
+ def score(instructions = nil, criteria = nil)
102
+ Score.new(instructions: instructions, criteria: criteria)
103
+ end
104
+
105
+ private
106
+
107
+ def merge_criteria(positional, named)
108
+ return positional if named.empty?
109
+ raise ArgumentError, "pass criteria either positionally or as keywords, not both" unless positional.nil?
110
+
111
+ named
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,77 @@
1
+ module TypeSafe
2
+ module HTTP
3
+ # Anything that can send a Request and return a Response, such as NetHTTPTransport.
4
+ interface _Transport
5
+ def call: (Request request) -> Response
6
+ end
7
+
8
+ class Request
9
+ attr_reader method: Symbol
10
+ attr_reader uri: URI::HTTP
11
+ attr_reader headers: Hash[String, String]
12
+ attr_reader body: String?
13
+ attr_reader timeout: Numeric
14
+
15
+ def initialize: (method: Symbol, uri: URI::HTTP, headers: Hash[String, String], body: String?, timeout: Numeric) -> void
16
+ def endpoint: () -> String
17
+ def with_headers: (Hash[String, String] extra) -> Request
18
+ end
19
+
20
+ class Response
21
+ attr_reader status: Integer
22
+ attr_reader headers: Hash[String, String]
23
+ attr_reader body: String
24
+ attr_reader request: Request?
25
+
26
+ def initialize: (status: Integer, headers: Hash[untyped, untyped], body: String?, ?request: Request?) -> void
27
+ def []: (String | Symbol name) -> String?
28
+ def request_id: () -> String?
29
+ def success?: () -> bool
30
+ def json: () -> untyped
31
+ def retry_after: () -> Float?
32
+ end
33
+
34
+ class ConnectionManager
35
+ IDLE_TIMEOUT: Integer
36
+
37
+ def initialize: (?clock: (^() -> Float)?) -> void
38
+ def connection_for: (URI::HTTP uri, timeout: Numeric) -> Net::HTTP
39
+ def discard: (URI::HTTP uri) -> void
40
+ def clear: () -> void
41
+ def size: () -> Integer
42
+ end
43
+
44
+ class NetHTTPTransport
45
+ TIMEOUT_ERRORS: Array[singleton(Exception)]
46
+ CONNECTION_ERRORS: Array[singleton(Exception)]
47
+
48
+ def initialize: () -> void
49
+ def call: (Request request) -> Response
50
+ def close: () -> void
51
+ def connection_manager: () -> ConnectionManager
52
+ end
53
+
54
+ class Retrier
55
+ MONOTONIC: Integer
56
+
57
+ attr_reader policy: RetryPolicy
58
+
59
+ def initialize: (RetryPolicy policy, ?sleeper: (^(Numeric) -> void)?, ?clock: (^() -> Float)?,
60
+ ?random: RetryPolicy::_Random, ?on_retry: (^(Exception, Integer, Numeric) -> void)?) -> void
61
+ def run: [T] () { (Integer attempt) -> T } -> T
62
+ end
63
+
64
+ class Requestor
65
+ PROTECTED_HEADERS: Array[String]
66
+
67
+ attr_reader config: Configuration
68
+ attr_reader transport: _Transport
69
+
70
+ def initialize: (Configuration config, ?transport: _Transport?, ?sleeper: (^(Numeric) -> void)?,
71
+ ?clock: (^() -> Float)?) -> void
72
+ def post: (String path, body: Hash[String, untyped], ?options: RequestOptions) -> Response
73
+ def get: (String path, ?options: RequestOptions) -> Response
74
+ def close: () -> void
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,48 @@
1
+ module TypeSafe
2
+ module Questions
3
+ class Question
4
+ attr_reader type: String
5
+ attr_reader instructions: json_content?
6
+ attr_reader criteria: untyped
7
+
8
+ def initialize: (String type, instructions: json_input?, criteria: untyped) -> void
9
+ def to_h: () -> Hash[String, untyped]
10
+ def to_json: (*untyped args) -> String
11
+ def ==: (untyped other) -> bool
12
+ alias eql? ==
13
+ def hash: () -> Integer
14
+ end
15
+
16
+ class Noul < Question
17
+ TYPE: String
18
+ KEYS: Array[String]
19
+
20
+ def initialize: (?instructions: json_input?, ?criteria: Hash[untyped, untyped]?) -> void
21
+ def self.normalize_criteria: (Hash[untyped, untyped]? criteria) -> Hash[String, json_content?]?
22
+ end
23
+
24
+ class Choice < Question
25
+ TYPE: String
26
+
27
+ def initialize: (?instructions: json_input?, criteria: Hash[untyped, untyped]) -> void
28
+ def self.normalize_criteria: (Hash[untyped, untyped] criteria) -> Hash[String, json_content?]
29
+ end
30
+
31
+ class Score < Question
32
+ TYPE: String
33
+ MIN_LEVELS: Integer
34
+
35
+ def initialize: (?instructions: json_input?, criteria: Array[untyped]) -> void
36
+ def self.normalize_criteria: (Array[untyped] criteria) -> Array[json_content]
37
+ end
38
+
39
+ module Normalizer
40
+ TYPES: Hash[String, singleton(Question)]
41
+
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]
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,131 @@
1
+ module TypeSafe
2
+ module Responses
3
+ class InvalidField < StandardError
4
+ attr_reader field_path: String
5
+
6
+ def initialize: (String field_path, String message) -> void
7
+ end
8
+
9
+ module Reader
10
+ def self?.validation_error: (InvalidField error, HTTP::Response response, untyped json) -> APIResponseValidationError
11
+ def self?.hash!: (untyped value, String path) -> Hash[String, untyped]
12
+ def self?.string!: (untyped value, String path) -> String
13
+ def self?.number!: (untyped value, String path) -> Float
14
+ def self?.optional_integer: (untyped value, String path) -> Integer?
15
+ def self?.optional_string: (untyped value, String path) -> String?
16
+ def self?.number_map!: (untyped value, String path) -> Hash[String, Float]
17
+ def self?.integer_keyed: (Hash[String, untyped] hash, String path) -> Hash[Integer, untyped]
18
+ end
19
+
20
+ class Usage
21
+ attr_reader input_tokens: Integer?
22
+ attr_reader output_tokens: Integer?
23
+
24
+ def self.from_hash: (untyped hash, ?String path) -> Usage
25
+ def initialize: (?input_tokens: Integer?, ?output_tokens: Integer?) -> void
26
+ def to_h: () -> Hash[String, Integer?]
27
+ def ==: (untyped other) -> bool
28
+ alias eql? ==
29
+ def hash: () -> Integer
30
+ end
31
+
32
+ class Answer
33
+ TYPES: Hash[String, singleton(Answer)]
34
+
35
+ attr_reader type: String
36
+ attr_reader raw: Hash[String, untyped]
37
+
38
+ def self.from_hash: (untyped hash, String path) -> Answer
39
+ def initialize: (Hash[String, untyped] raw) -> void
40
+ def to_h: () -> Hash[String, untyped]
41
+ def to_json: (*untyped args) -> String
42
+ def ==: (untyped other) -> bool
43
+ alias eql? ==
44
+ def hash: () -> Integer
45
+ end
46
+
47
+ class NoulAnswer < Answer
48
+ attr_reader noul: Float
49
+
50
+ def self.parse: (Hash[String, untyped] data, String path) -> NoulAnswer
51
+ def initialize: (Hash[String, untyped] raw, noul: Float) -> void
52
+ end
53
+
54
+ class ChoiceAnswer < Answer
55
+ attr_reader choice: String
56
+ attr_reader probabilities: IndifferentHash
57
+ attr_reader confidence: Float
58
+
59
+ def self.parse: (Hash[String, untyped] data, String path) -> ChoiceAnswer
60
+ def initialize: (Hash[String, untyped] raw, choice: String, probabilities: Hash[String, Float], confidence: Float) -> void
61
+ end
62
+
63
+ class ScoreAnswer < Answer
64
+ attr_reader score: Float
65
+ attr_reader legend: Hash[Integer, json_content]
66
+ attr_reader probabilities: Hash[Integer, Float]
67
+ attr_reader confidence: Float
68
+
69
+ def self.parse: (Hash[String, untyped] data, String path) -> ScoreAnswer
70
+ def initialize: (Hash[String, untyped] raw, score: Float, legend: Hash[Integer, untyped],
71
+ probabilities: Hash[Integer, Float], confidence: Float) -> void
72
+ def raw_legend: () -> Hash[String, untyped]
73
+ def raw_probabilities: () -> Hash[String, Numeric]
74
+ end
75
+
76
+ class UnknownAnswer < Answer
77
+ def self.parse: (Hash[String, untyped] data, String path) -> UnknownAnswer
78
+ end
79
+
80
+ class SystemOneResponse
81
+ attr_reader model: String
82
+ attr_reader usage: Usage
83
+ attr_reader answers: IndifferentHash
84
+ attr_reader http_response: HTTP::Response?
85
+ attr_reader raw: Hash[String, untyped]?
86
+
87
+ def self.from_http: (HTTP::Response response) -> SystemOneResponse
88
+ def self.parse: (untyped json, HTTP::Response? response) -> SystemOneResponse
89
+ def self.parse_answers: (untyped answers) -> Hash[String, Answer]
90
+ def initialize: (model: String, usage: Usage, answers: Hash[untyped, Answer], ?http_response: HTTP::Response?,
91
+ ?raw: Hash[String, untyped]?) -> void
92
+ def []: (String | Symbol name) -> Answer?
93
+ def request_id: () -> String?
94
+ def nouls: () -> IndifferentHash
95
+ def choices: () -> IndifferentHash
96
+ def scores: () -> IndifferentHash
97
+ def to_h: () -> Hash[String, untyped]
98
+ def to_json: (*untyped args) -> String
99
+ end
100
+
101
+ class ModelCard
102
+ attr_reader name: String
103
+ attr_reader description: String?
104
+ attr_reader release_date: String?
105
+ attr_reader raw: Hash[String, untyped]
106
+
107
+ def self.from_hash: (untyped hash, String path) -> ModelCard
108
+ def initialize: (Hash[String, untyped] raw, name: String, ?description: String?, ?release_date: String?) -> void
109
+ def to_h: () -> Hash[String, untyped]
110
+ def ==: (untyped other) -> bool
111
+ alias eql? ==
112
+ def hash: () -> Integer
113
+ end
114
+
115
+ class ListModelsResponse
116
+ include Enumerable[ModelCard]
117
+
118
+ attr_reader models: Array[ModelCard]
119
+ attr_reader http_response: HTTP::Response?
120
+ attr_reader raw: Hash[String, untyped]?
121
+
122
+ def self.from_http: (HTTP::Response response) -> ListModelsResponse
123
+ def self.parse_models: (untyped list) -> Array[ModelCard]
124
+ def initialize: (models: Array[ModelCard], ?http_response: HTTP::Response?, ?raw: Hash[String, untyped]?) -> void
125
+ def each: () { (ModelCard) -> void } -> void
126
+ def names: () -> Array[String]
127
+ def request_id: () -> String?
128
+ def to_h: () -> Hash[String, untyped]
129
+ end
130
+ end
131
+ end