typesafe-jev 0.2.0 → 0.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7645167a374f6815e9a4ee6b96b56d54f611a3a08b274467f963dedd66e786ca
4
- data.tar.gz: 0d66633fe64b5061b9f1bf7cb59fdf74f73d711c332bb34f7bbf0be1a042b85c
3
+ metadata.gz: 56d8b9176783b02dfd76aa0c59e960fc25d2b936e852cdfa79658578ac5e4e9e
4
+ data.tar.gz: d534502c6f03804f0f96ea4839dc24c8fe28724e285f1fac53dcabe2fe5f7f8f
5
5
  SHA512:
6
- metadata.gz: 6de70dbd0412a10808feb2f79e1635462056a0c8069306b3fd063167b15ff0e6706f64fc22719e3cdd47f21a3fff4a70c64d368660eb1491c5abd5b4240e7baf
7
- data.tar.gz: fd391b3ecd54101ea40a0923d618ee6e9a884ae54c3c9876d9e3ef5ed3f3a8741b81b7ae032ebe3cf9e109cdf96df468bd3741995c94a6d0b949266ed2711622
6
+ metadata.gz: 034e9bdc3ae16ef9783612d7eea2ee524a3b457cfb666ce1095c5c43224dc148d1ce9442cd65c7b4a1f6eb0e8d5b88dcf32d3dfe1b0fe5b5e7c5e74f0df5a5b1
7
+ data.tar.gz: 10fcd78136555393c85d8876a31ac9584f61ce4464b7818d7c537698426b381545a53a768c99e1313d62cc74e19183e590b0aa5f434ab61465a5d36a01aa98eb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1
4
+
5
+ - Fix gemspec metadata: point `source_code_uri`/`changelog_uri` and author/email at the `dtheofr` GitHub account.
6
+
7
+ ## 0.4.0
8
+
9
+ - Add `Typesafe::Jev`, a `Typesafe::Client` subclass with the model pinned to `jev-latest`: `Jev.new(api_key: nil).evaluate(...)` plus a one-shot `Typesafe::Jev.evaluate(state:, questions:, api_key: nil)`.
10
+
11
+ ## 0.3.0
12
+
13
+ - Add `Typesafe::Client`: `initialize(api_key:, model:)` (key from argument or `TYPESAFE_API_KEY`, model defaults to `jev-latest`) and `evaluate(state:, questions:, model: nil)` posting to the System One endpoint. Returns the parsed JSON response; non-2xx responses raise the stdlib `Net::HTTP` error as-is.
14
+
3
15
  ## 0.2.0
4
16
 
5
17
  - Add question classes: `Typesafe::Noul`, `Typesafe::Choice`, `Typesafe::Score` with the abstract `Typesafe::Question` base. Frozen, immutable value objects that validate their inputs and serialize to the TypeSafe API shape via `#to_h`/`#to_json`.
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+
6
+ require_relative "question"
7
+
8
+ module Typesafe
9
+ # HTTP client for the TypeSafe System One evaluation endpoint.
10
+ #
11
+ # client = Typesafe::Client.new(api_key: "sk-...", model: "jev-latest")
12
+ # client.evaluate(
13
+ # state: { ticket: { text: "My payouts failed" } },
14
+ # questions: { refund_requested: Typesafe::Noul.new("Does the customer request a refund?") }
15
+ # )
16
+ #
17
+ # The API key comes from the +TYPESAFE_API_KEY+ environment variable unless
18
+ # passed at initialization. The model defaults to +"jev-latest"+ and may be
19
+ # overridden per call.
20
+ class Client
21
+ DEFAULT_MODEL = "jev-latest"
22
+ API_ENDPOINT = "https://api.typesafe.ai/v1/systemone"
23
+
24
+ # @return [String] the API key sent as +Authorization: Bearer <key>+.
25
+ attr_reader :api_key
26
+
27
+ # @return [String] the default model used when +evaluate+ gets none.
28
+ attr_reader :model
29
+
30
+ # @param api_key [String, nil] the TypeSafe API key; falls back to the
31
+ # +TYPESAFE_API_KEY+ environment variable.
32
+ # @param model [String, nil] the default model; defaults to "jev-latest".
33
+ # @raise [ArgumentError] if no usable API key is found or +model+ is
34
+ # present but not a non-empty String.
35
+ def initialize(api_key: nil, model: nil)
36
+ @api_key = freeze_string(api_key || ENV.fetch("TYPESAFE_API_KEY") { nil },
37
+ "api_key must be a non-empty String " \
38
+ "(passed at initialization or set in the TYPESAFE_API_KEY environment variable)")
39
+ @model = freeze_string(model || DEFAULT_MODEL, "model must be a non-empty String")
40
+ freeze
41
+ end
42
+
43
+ # Evaluates a state against a map of questions and returns the raw JSON
44
+ # response as a Hash, e.g. +{"model"=>..., "answers"=>{...}, "usage"=>...}+.
45
+ #
46
+ # @param state [String, Object, Array] the content to evaluate; passed
47
+ # through as-is.
48
+ # @param questions [Hash{String, Symbol => Question}] question ids mapped
49
+ # to Question objects; answers come back under the same keys.
50
+ # @param model [String, nil] model for this call; falls back to the model
51
+ # given at initialization.
52
+ # @return [Hash] the parsed response body.
53
+ # @raise [ArgumentError] if +questions+ is not a non-empty Hash of
54
+ # String/Symbol keys to Question values, or +model+ is invalid.
55
+ # @raise [Net::HTTPClientException, Net::HTTPFatalError] on any non-2xx
56
+ # HTTP response.
57
+ def evaluate(state:, questions:, model: nil)
58
+ model = model.nil? ? self.model : freeze_string(model, "model must be a non-empty String")
59
+ questions = validate_questions!(questions)
60
+
61
+ body = JSON.generate(
62
+ state: state,
63
+ model: model,
64
+ questions: questions.transform_values(&:to_h)
65
+ )
66
+
67
+ response = post(body)
68
+ response.value
69
+ JSON.parse(response.body)
70
+ end
71
+
72
+ private
73
+
74
+ def post(body)
75
+ uri = URI(API_ENDPOINT)
76
+ request = Net::HTTP::Post.new(uri)
77
+ request["Authorization"] = "Bearer #{api_key}"
78
+ request["Content-Type"] = "application/json"
79
+ request.body = body
80
+
81
+ Net::HTTP.start(
82
+ uri.host, uri.port,
83
+ use_ssl: true,
84
+ open_timeout: 5, write_timeout: 10, read_timeout: 30
85
+ ) { |http| http.request(request) }
86
+ end
87
+
88
+ def validate_questions!(questions)
89
+ unless questions.is_a?(Hash) && !questions.empty?
90
+ raise ArgumentError, "questions must be a non-empty Hash mapping question ids to Question objects"
91
+ end
92
+
93
+ questions.each do |id, question|
94
+ unless (id.is_a?(String) && !id.strip.empty?) || (id.is_a?(Symbol) && !id.to_s.strip.empty?)
95
+ raise ArgumentError, "questions keys must be non-empty String or Symbol question ids, got #{id.inspect}"
96
+ end
97
+
98
+ unless question.is_a?(Question)
99
+ raise ArgumentError, "questions values must be Typesafe::Question objects, got #{question.inspect}"
100
+ end
101
+ end
102
+
103
+ questions
104
+ end
105
+
106
+ def freeze_string(value, error_message)
107
+ unless value.is_a?(String) && !value.strip.empty?
108
+ raise ArgumentError, error_message
109
+ end
110
+
111
+ value.dup.freeze
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "client"
4
+
5
+ module Typesafe
6
+ # Convenience facade over {Client} for TypeSafe's flagship model, Jev.
7
+ #
8
+ # The model is pinned to "jev-latest"; for any other model, use
9
+ # {Typesafe::Client} directly.
10
+ #
11
+ # Typesafe::Jev.evaluate(state: "My payouts failed", questions: { urgent: Typesafe::Noul.new("Is this urgent?") })
12
+ #
13
+ # or, reusing one client:
14
+ #
15
+ # jev = Typesafe::Jev.new
16
+ # jev.evaluate(state: state, questions: questions)
17
+ class Jev < Client
18
+ PINNED_MODEL = "jev-latest"
19
+
20
+ # @param api_key [String, nil] the TypeSafe API key; falls back to the
21
+ # +TYPESAFE_API_KEY+ environment variable.
22
+ # @raise [ArgumentError] if no usable API key is found.
23
+ def initialize(api_key: nil)
24
+ super(api_key: api_key, model: PINNED_MODEL)
25
+ end
26
+
27
+ # Evaluates a state against a map of questions with the pinned model.
28
+ #
29
+ # @param state [String, Object, Array] the content to evaluate; passed
30
+ # through as-is.
31
+ # @param questions [Hash{String, Symbol => Question}] question ids mapped
32
+ # to Question objects; answers come back under the same keys.
33
+ # @param model [String, nil] must be nil or "jev-latest".
34
+ # @return [Hash] the parsed response body.
35
+ # @raise [ArgumentError] if +questions+ is invalid or +model+ is not the
36
+ # pinned one.
37
+ # @raise [Net::HTTPClientException, Net::HTTPFatalError] on any non-2xx
38
+ # HTTP response.
39
+ def evaluate(state:, questions:, model: nil)
40
+ if model && model != PINNED_MODEL
41
+ raise ArgumentError,
42
+ "Typesafe::Jev pins the model to \"jev-latest\"; use Typesafe::Client for other models"
43
+ end
44
+
45
+ super(state: state, questions: questions, model: model)
46
+ end
47
+
48
+ # One-shot evaluation using a client built from the environment (or the
49
+ # given API key). Equivalent to <tt>Jev.new(api_key:).evaluate(...)</tt>.
50
+ #
51
+ # @param state [String, Object, Array] the content to evaluate.
52
+ # @param questions [Hash{String, Symbol => Question}] question ids mapped
53
+ # to Question objects.
54
+ # @param api_key [String, nil] the TypeSafe API key; falls back to the
55
+ # +TYPESAFE_API_KEY+ environment variable.
56
+ # @return [Hash] the parsed response body.
57
+ def self.evaluate(state:, questions:, api_key: nil)
58
+ new(api_key: api_key).evaluate(state: state, questions: questions)
59
+ end
60
+ end
61
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Typesafe
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.1"
5
5
  end
data/lib/typesafe.rb CHANGED
@@ -7,6 +7,8 @@ require_relative "typesafe/question"
7
7
  require_relative "typesafe/noul"
8
8
  require_relative "typesafe/choice"
9
9
  require_relative "typesafe/score"
10
+ require_relative "typesafe/client"
11
+ require_relative "typesafe/jev"
10
12
 
11
13
  module Typesafe
12
14
  end
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typesafe-jev
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
- - dtheo
7
+ - dtheofr
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
@@ -37,9 +37,23 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '3.13'
40
+ - !ruby/object:Gem::Dependency
41
+ name: webmock
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
40
54
  description: Use Jev, TypeSafe's System One model, from Ruby or Rails.
41
55
  email:
42
- - dtheo@users.noreply.github.com
56
+ - dtheofr@users.noreply.github.com
43
57
  executables: []
44
58
  extensions: []
45
59
  extra_rdoc_files: []
@@ -49,6 +63,8 @@ files:
49
63
  - README.md
50
64
  - lib/typesafe.rb
51
65
  - lib/typesafe/choice.rb
66
+ - lib/typesafe/client.rb
67
+ - lib/typesafe/jev.rb
52
68
  - lib/typesafe/noul.rb
53
69
  - lib/typesafe/question.rb
54
70
  - lib/typesafe/score.rb
@@ -58,8 +74,8 @@ licenses:
58
74
  - MIT
59
75
  metadata:
60
76
  homepage_uri: https://docs.typesafe.ai
61
- source_code_uri: https://github.com/dtheo/typesafe-jev-ruby
62
- changelog_uri: https://github.com/dtheo/typesafe-jev-ruby/blob/main/CHANGELOG.md
77
+ source_code_uri: https://github.com/dtheofr/typesafe-jev-ruby
78
+ changelog_uri: https://github.com/dtheofr/typesafe-jev-ruby/blob/main/CHANGELOG.md
63
79
  rdoc_options: []
64
80
  require_paths:
65
81
  - lib