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 +4 -4
- data/CHANGELOG.md +12 -0
- data/lib/typesafe/client.rb +114 -0
- data/lib/typesafe/jev.rb +61 -0
- data/lib/typesafe/version.rb +1 -1
- data/lib/typesafe.rb +2 -0
- metadata +21 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 56d8b9176783b02dfd76aa0c59e960fc25d2b936e852cdfa79658578ac5e4e9e
|
|
4
|
+
data.tar.gz: d534502c6f03804f0f96ea4839dc24c8fe28724e285f1fac53dcabe2fe5f7f8f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/typesafe/jev.rb
ADDED
|
@@ -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
|
data/lib/typesafe/version.rb
CHANGED
data/lib/typesafe.rb
CHANGED
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.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
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
|
-
-
|
|
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/
|
|
62
|
-
changelog_uri: https://github.com/
|
|
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
|