ruby_llm-providers-typesafe 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.
- checksums.yaml +7 -0
- data/.flayignore +1 -0
- data/.github/workflows/ci.yml +32 -0
- data/.github/workflows/gitleaks.yml +22 -0
- data/.github/workflows/release.yml +33 -0
- data/.overcommit.yml +31 -0
- data/.rspec +2 -0
- data/.rubocop.yml +35 -0
- data/Archspec.rb +23 -0
- data/LICENSE +21 -0
- data/README.md +84 -0
- data/lib/ruby_llm/providers/typesafe/system_one/evaluations.rb +95 -0
- data/lib/ruby_llm/providers/typesafe/system_one/models.rb +35 -0
- data/lib/ruby_llm/providers/typesafe/system_one/rerank.rb +57 -0
- data/lib/ruby_llm/providers/typesafe/system_one.rb +19 -0
- data/lib/ruby_llm/providers/typesafe.rb +68 -0
- data/lib/ruby_llm/typesafe/evaluation.rb +75 -0
- data/lib/ruby_llm/typesafe/questions.rb +74 -0
- data/lib/ruby_llm/typesafe.rb +47 -0
- data/models.json +62 -0
- data/spec/fixtures/vcr_cassettes/rubyllm_rerank_typesafe_jev_latest_orders_documents_by_relevance.yml +49 -0
- data/spec/fixtures/vcr_cassettes/rubyllm_typesafe_evaluation_typesafe_jev_latest_answers_noul_choice_and_score_questions.yml +46 -0
- data/spec/ruby_llm/models_spec.rb +11 -0
- data/spec/ruby_llm/providers/typesafe/system_one_spec.rb +162 -0
- data/spec/ruby_llm/providers/typesafe_spec.rb +58 -0
- data/spec/ruby_llm/rerank_request_spec.rb +38 -0
- data/spec/ruby_llm/rerank_spec.rb +23 -0
- data/spec/ruby_llm/typesafe/evaluation_spec.rb +27 -0
- data/spec/ruby_llm/typesafe_spec.rb +60 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/models.rb +9 -0
- data/spec/support/rubyllm_configuration.rb +14 -0
- data/spec/support/vcr_configuration.rb +16 -0
- metadata +91 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ed59a9e9d3a05dc3295e783fed03656a1c7991bc7569f6b3f998447d1ff0af13
|
|
4
|
+
data.tar.gz: 6bf922a4552c10571302adba8c53c05aefe3d684490adf259b0c885c8fd4b140
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9d4ac70d8ee3fd3f473abffc61d7c586dd2672b7492b1f3c3548e3c9449aa7707cf17f887caa8796550bf36906f8bfc3c633a90c7a47dfc1405fa1ce1bf56285
|
|
7
|
+
data.tar.gz: f8722a75e6928beba4d2ae6269aa133c580305fd91d6629e4987f487e8795c0bb8c24493de8771097e8995df9f5cf39feedd900f26a9c1be8496d6959be67c02
|
data/.flayignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
spec/support/streaming_error_helpers.rb
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
push:
|
|
7
|
+
branches: ["main"]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
name: Ruby ${{ matrix.ruby-version }}
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
ruby-version: ["3.1", "3.2", "3.3", "3.4", "4.0"]
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v6
|
|
22
|
+
|
|
23
|
+
- uses: ruby/setup-ruby@v1
|
|
24
|
+
with:
|
|
25
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
26
|
+
bundler-cache: true
|
|
27
|
+
|
|
28
|
+
- run: bundle exec rake
|
|
29
|
+
if: matrix.ruby-version != '3.1'
|
|
30
|
+
|
|
31
|
+
- run: bundle exec rake rubocop flay spec
|
|
32
|
+
if: matrix.ruby-version == '3.1'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Gitleaks
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
push:
|
|
7
|
+
branches: ["main"]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
scan:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
|
+
with:
|
|
18
|
+
fetch-depth: 0
|
|
19
|
+
|
|
20
|
+
- uses: gitleaks/gitleaks-action@v2
|
|
21
|
+
env:
|
|
22
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
name: Build and publish
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v6
|
|
18
|
+
|
|
19
|
+
- uses: ruby/setup-ruby@v1
|
|
20
|
+
with:
|
|
21
|
+
ruby-version: "3.4"
|
|
22
|
+
bundler-cache: true
|
|
23
|
+
|
|
24
|
+
- run: bundle exec rake
|
|
25
|
+
|
|
26
|
+
- name: Build gem
|
|
27
|
+
run: gem build ruby_llm-providers-typesafe.gemspec
|
|
28
|
+
|
|
29
|
+
- name: Configure RubyGems trusted publishing
|
|
30
|
+
uses: rubygems/configure-rubygems-credentials@v2.1.0
|
|
31
|
+
|
|
32
|
+
- name: Publish to RubyGems
|
|
33
|
+
run: gem push ruby_llm-providers-typesafe-*.gem
|
data/.overcommit.yml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
PreCommit:
|
|
2
|
+
RuboCop:
|
|
3
|
+
enabled: true
|
|
4
|
+
auto_correct: true
|
|
5
|
+
on_warn: fail
|
|
6
|
+
|
|
7
|
+
Flay:
|
|
8
|
+
enabled: true
|
|
9
|
+
include:
|
|
10
|
+
- lib/**/*.rb
|
|
11
|
+
- spec/**/*.rb
|
|
12
|
+
mass_threshold: 70
|
|
13
|
+
|
|
14
|
+
ArchSpec:
|
|
15
|
+
enabled: true
|
|
16
|
+
command: ['bundle', 'exec', 'archspec', 'check']
|
|
17
|
+
on_warn: fail
|
|
18
|
+
|
|
19
|
+
RSpec:
|
|
20
|
+
enabled: true
|
|
21
|
+
command: ['bundle', 'exec', 'rspec']
|
|
22
|
+
on_warn: fail
|
|
23
|
+
|
|
24
|
+
TrailingWhitespace:
|
|
25
|
+
enabled: true
|
|
26
|
+
auto_correct: true
|
|
27
|
+
|
|
28
|
+
Gitleaks:
|
|
29
|
+
enabled: true
|
|
30
|
+
required_executable: gitleaks
|
|
31
|
+
command: ['gitleaks', 'protect', '--staged', '--redact', '--verbose']
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
plugins:
|
|
2
|
+
- rubocop-performance
|
|
3
|
+
- rubocop-rake
|
|
4
|
+
- rubocop-rspec
|
|
5
|
+
|
|
6
|
+
AllCops:
|
|
7
|
+
NewCops: enable
|
|
8
|
+
TargetRubyVersion: 3.1
|
|
9
|
+
Exclude:
|
|
10
|
+
- Archspec.rb
|
|
11
|
+
- vendor/**/*
|
|
12
|
+
- gemfiles/**/*
|
|
13
|
+
SuggestExtensions: false
|
|
14
|
+
|
|
15
|
+
Metrics/AbcSize:
|
|
16
|
+
Max: 80
|
|
17
|
+
Metrics/MethodLength:
|
|
18
|
+
Max: 40
|
|
19
|
+
CountAsOne:
|
|
20
|
+
- array
|
|
21
|
+
- hash
|
|
22
|
+
- heredoc
|
|
23
|
+
Metrics/BlockLength:
|
|
24
|
+
Exclude:
|
|
25
|
+
- spec/**/*.rb
|
|
26
|
+
RSpec/ExampleLength:
|
|
27
|
+
Enabled: false
|
|
28
|
+
RSpec/MultipleExpectations:
|
|
29
|
+
Enabled: false
|
|
30
|
+
RSpec/MultipleMemoizedHelpers:
|
|
31
|
+
Max: 8
|
|
32
|
+
Naming/MethodParameterName:
|
|
33
|
+
AllowedNames:
|
|
34
|
+
- id
|
|
35
|
+
- 'no'
|
data/Archspec.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'lib/**/*.rb'
|
|
4
|
+
|
|
5
|
+
component :provider,
|
|
6
|
+
in: %w[
|
|
7
|
+
lib/ruby_llm/providers/typesafe.rb
|
|
8
|
+
lib/ruby_llm/providers/typesafe/**/*.rb
|
|
9
|
+
],
|
|
10
|
+
namespace: 'RubyLLM::Providers::Typesafe'
|
|
11
|
+
|
|
12
|
+
provider.cannot_reference_constants 'RSpec', 'WebMock', 'VCR'
|
|
13
|
+
|
|
14
|
+
preset :ruby_conventions
|
|
15
|
+
|
|
16
|
+
component :api,
|
|
17
|
+
in: %w[
|
|
18
|
+
lib/ruby_llm/typesafe.rb
|
|
19
|
+
lib/ruby_llm/typesafe/**/*.rb
|
|
20
|
+
],
|
|
21
|
+
namespace: 'RubyLLM::Typesafe'
|
|
22
|
+
|
|
23
|
+
api.cannot_reference_constants 'RubyLLM::Providers', 'RSpec', 'WebMock', 'VCR'
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Javier Gradiche
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# ruby_llm-providers-typesafe
|
|
2
|
+
|
|
3
|
+
[TypeSafe](https://typesafe.ai) for [RubyLLM](https://rubyllm.com). TypeSafe's System One models, starting with Jev, answer typed questions about your data with calibrated probabilities instead of generated text.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
gem 'ruby_llm-providers-typesafe'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
RubyLLM.configure do |config|
|
|
13
|
+
config.typesafe_api_key = ENV['TYPESAFE_API_KEY']
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Outside Bundler, load it with `require 'ruby_llm/providers/typesafe'`.
|
|
18
|
+
|
|
19
|
+
## Evaluate
|
|
20
|
+
|
|
21
|
+
Ask questions about a state, and get one typed answer per question:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
evaluation = RubyLLM::Typesafe.evaluate("Help! My payouts have been failing for 3 days.") do |q|
|
|
25
|
+
q.noul :urgent, "Does this convey urgency?"
|
|
26
|
+
q.choice :team, "Which team should handle this?",
|
|
27
|
+
billing: "Payments, invoicing, refunds",
|
|
28
|
+
technical: "Bugs, outages, integrations",
|
|
29
|
+
sales: "Pricing, upgrades, new accounts"
|
|
30
|
+
q.score :frustration, "How frustrated is the customer?", ["Calm", "Frustrated", "Very angry"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
evaluation[:urgent].probability # => 0.92
|
|
34
|
+
evaluation[:urgent].yes? # => true
|
|
35
|
+
evaluation[:team].option # => :technical
|
|
36
|
+
evaluation[:team].probabilities # => {billing: 0.08, technical: 0.85, sales: 0.07}
|
|
37
|
+
evaluation[:team].confidence # => 0.82
|
|
38
|
+
evaluation[:frustration].score # => 1.6
|
|
39
|
+
evaluation[:frustration].level # => "Very angry"
|
|
40
|
+
evaluation.model # => "jev-1.13.0"
|
|
41
|
+
evaluation.cost.total # => 0.0000131
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
There are three question types:
|
|
45
|
+
|
|
46
|
+
* `noul` asks a yes/no question. Its answer is the probability of yes. Pass `yes:` and `no:` to describe what each answer means.
|
|
47
|
+
* `choice` picks one option. Pass descriptions as keywords or a Hash, or an Array of options that need none. The answer uses the keys you passed.
|
|
48
|
+
* `score` rates the state on ordered levels, lowest first. The answer can land between levels.
|
|
49
|
+
|
|
50
|
+
The state can be a String, or a Hash or Array for structured data. Refer to parts of it from your questions with backticked paths such as `` `ticket.messages[0].text` ``. Every question runs in parallel against the same state, so ask independent questions together.
|
|
51
|
+
|
|
52
|
+
`yes?` compares the probability to 0.5 by default. Choose thresholds from your own data with `yes?(threshold: 0.8)`, and use `confidence` to send uncertain choices and scores to a person. See TypeSafe's guides to [writing questions](https://docs.typesafe.ai/concepts/how-to-build-with-system-one) and [confidence](https://docs.typesafe.ai/confidence).
|
|
53
|
+
|
|
54
|
+
Evaluations default to `jev-latest`. Pin a version once you have tuned thresholds against it:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
RubyLLM::Typesafe.evaluate(ticket, model: "jev-1.13.0") { |q| q.noul :spam, "Is this message spam?" }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Rerank
|
|
61
|
+
|
|
62
|
+
Jev also works with `RubyLLM.rerank`. Every document gets a relevance question in a single request, and its score is the probability that it helps answer the query:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
rerank = RubyLLM.rerank("What is the capital of the United States?",
|
|
66
|
+
["Carson City is the capital of Nevada.",
|
|
67
|
+
"Washington, D.C. is the capital of the United States."],
|
|
68
|
+
model: "jev-latest", provider: :typesafe, top_n: 1)
|
|
69
|
+
|
|
70
|
+
rerank.results.first.document # => "Washington, D.C. is the capital of the United States."
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The query and all documents share Jev's 32k-token state budget. Rerank a shortlist from a faster search, not a whole corpus.
|
|
74
|
+
|
|
75
|
+
## Development
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
bin/setup
|
|
79
|
+
bundle exec rake
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Copy `.env.example` to `.env` and set `TYPESAFE_API_KEY` to record VCR cassettes. The first local run calls the API and records them; CI only replays committed cassettes. A failing live example deletes its cassette so the next run hits the API again.
|
|
83
|
+
|
|
84
|
+
`bundle exec rake models` refreshes `models.json` from TypeSafe's model listing. The listing returns aliases such as `jev-latest`; versioned ids such as `jev-1.13.0` are accepted without being listed.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class Typesafe < Provider
|
|
6
|
+
class SystemOne < Protocol
|
|
7
|
+
# Renders Typesafe::Questions to the wire and parses the answers
|
|
8
|
+
# into a Typesafe::Evaluation.
|
|
9
|
+
module Evaluations
|
|
10
|
+
def evaluation_url
|
|
11
|
+
'v1/systemone'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def evaluate(state, questions, model:)
|
|
15
|
+
response = @connection.post evaluation_url, render_evaluation_payload(state, questions, model:)
|
|
16
|
+
parse_evaluation_response(response, questions)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def render_evaluation_payload(state, questions, model:)
|
|
20
|
+
{
|
|
21
|
+
state: state,
|
|
22
|
+
model: model,
|
|
23
|
+
questions: questions.to_h.to_h { |id, question| [id.to_s, render_question(question)] }
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def render_question(question)
|
|
28
|
+
rendered = { type: question.type.to_s, instructions: question.instructions }
|
|
29
|
+
criteria = render_criteria(question)
|
|
30
|
+
criteria ? rendered.merge(criteria: criteria) : rendered
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def parse_evaluation_response(response, questions)
|
|
34
|
+
data = response.body
|
|
35
|
+
answers = questions.to_h.to_h do |id, question|
|
|
36
|
+
[id, parse_answer(question, data.dig('answers', id.to_s) || {})]
|
|
37
|
+
end
|
|
38
|
+
usage = data['usage'] || {}
|
|
39
|
+
|
|
40
|
+
RubyLLM::Typesafe::Evaluation.new(
|
|
41
|
+
answers: answers,
|
|
42
|
+
model: data['model'],
|
|
43
|
+
tokens: Tokens.new(input: usage['input_tokens'], output: usage['output_tokens']),
|
|
44
|
+
model_info: @model,
|
|
45
|
+
raw: data
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parse_answer(question, data)
|
|
50
|
+
case question.type
|
|
51
|
+
when :noul then RubyLLM::Typesafe::Evaluation::Noul.new(probability: data['noul'])
|
|
52
|
+
when :choice then parse_choice_answer(question, data)
|
|
53
|
+
when :score then parse_score_answer(question, data)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def render_criteria(question)
|
|
60
|
+
case question.type
|
|
61
|
+
when :noul
|
|
62
|
+
criteria = { true => question.criteria[:yes], false => question.criteria[:no] }.compact
|
|
63
|
+
criteria unless criteria.empty?
|
|
64
|
+
when :choice then question.criteria.to_h { |option, description| [option.to_s, description] }
|
|
65
|
+
when :score then question.criteria
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_choice_answer(question, data)
|
|
70
|
+
options = question.criteria.keys.to_h { |option| [option.to_s, option] }
|
|
71
|
+
|
|
72
|
+
RubyLLM::Typesafe::Evaluation::Choice.new(
|
|
73
|
+
option: options.fetch(data['choice'], data['choice']),
|
|
74
|
+
probabilities: (data['probabilities'] || {}).to_h do |option, value|
|
|
75
|
+
[options.fetch(option, option), value]
|
|
76
|
+
end,
|
|
77
|
+
confidence: data['confidence']
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def parse_score_answer(question, data)
|
|
82
|
+
levels = question.criteria
|
|
83
|
+
|
|
84
|
+
RubyLLM::Typesafe::Evaluation::Score.new(
|
|
85
|
+
score: data['score'],
|
|
86
|
+
levels: levels,
|
|
87
|
+
probabilities: levels.each_index.map { |index| data.dig('probabilities', index.to_s) },
|
|
88
|
+
confidence: data['confidence']
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class Typesafe < Provider
|
|
6
|
+
class SystemOne < Protocol
|
|
7
|
+
# Model listing for the System One API. Every model is billed per
|
|
8
|
+
# input token; output tokens are free.
|
|
9
|
+
module Models
|
|
10
|
+
INPUT_PRICE_PER_MILLION = 0.042
|
|
11
|
+
|
|
12
|
+
def models_url
|
|
13
|
+
'v1/models'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def parse_list_models_response(response, slug)
|
|
17
|
+
Array(response.body['models']).map do |model_data|
|
|
18
|
+
Model.new(
|
|
19
|
+
id: model_data['name'],
|
|
20
|
+
name: model_data['name'],
|
|
21
|
+
provider: slug,
|
|
22
|
+
created_at: model_data['release_date'],
|
|
23
|
+
modalities: { input: ['text'], output: ['rerank'] },
|
|
24
|
+
capabilities: [],
|
|
25
|
+
pricing: { text_tokens: { standard: { input_per_million: INPUT_PRICE_PER_MILLION,
|
|
26
|
+
output_per_million: 0 } } },
|
|
27
|
+
metadata: { description: model_data['description'] }.compact
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class Typesafe < Provider
|
|
6
|
+
class SystemOne < Protocol
|
|
7
|
+
# Reranking as one request: the query and documents form the state,
|
|
8
|
+
# and each document gets its own relevance question. The noul, the
|
|
9
|
+
# probability that the document is relevant, is its score.
|
|
10
|
+
module Rerank
|
|
11
|
+
RELEVANCE_CRITERIA = {
|
|
12
|
+
true => 'The document contains information that answers or directly addresses the query.',
|
|
13
|
+
false => 'The document is off topic or only shares words with the query.'
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def rerank_url
|
|
17
|
+
evaluation_url
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render_rerank_payload(query, documents, model:, top_n: nil, provider_options: {})
|
|
21
|
+
@top_n = top_n
|
|
22
|
+
questions = documents.each_index.to_h do |index|
|
|
23
|
+
[rerank_question_id(index), { type: 'noul', instructions: relevance_instructions(index),
|
|
24
|
+
criteria: RELEVANCE_CRITERIA }]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
{ state: { query: query, documents: documents }, model: model, questions: questions }
|
|
28
|
+
.merge(provider_options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def parse_rerank_response(response, model:, documents: [])
|
|
32
|
+
data = response.body
|
|
33
|
+
results = documents.each_with_index.map do |document, index|
|
|
34
|
+
RubyLLM::Rerank::Result.new(index: index, document: document,
|
|
35
|
+
score: data.dig('answers', rerank_question_id(index), 'noul'))
|
|
36
|
+
end
|
|
37
|
+
results = results.sort_by { |result| -result.score.to_f }
|
|
38
|
+
results = results.first(@top_n) if @top_n
|
|
39
|
+
|
|
40
|
+
RubyLLM::Rerank.new(results: results, model: data['model'] || model, raw: data,
|
|
41
|
+
input_tokens: data.dig('usage', 'input_tokens'))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def rerank_question_id(index)
|
|
47
|
+
"document_#{index}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def relevance_instructions(index)
|
|
51
|
+
"Does `documents[#{index}]` help answer `query`?"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'system_one/models'
|
|
4
|
+
require_relative 'system_one/evaluations'
|
|
5
|
+
require_relative 'system_one/rerank'
|
|
6
|
+
|
|
7
|
+
module RubyLLM
|
|
8
|
+
module Providers
|
|
9
|
+
class Typesafe < Provider
|
|
10
|
+
# The System One wire format: a state and a map of typed questions go
|
|
11
|
+
# in, one typed answer per question comes back.
|
|
12
|
+
class SystemOne < Protocol
|
|
13
|
+
include Models
|
|
14
|
+
include Evaluations
|
|
15
|
+
include Rerank
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_llm'
|
|
4
|
+
require_relative '../typesafe'
|
|
5
|
+
require_relative 'typesafe/system_one'
|
|
6
|
+
|
|
7
|
+
module RubyLLM
|
|
8
|
+
module Providers
|
|
9
|
+
# TypeSafe API integration. TypeSafe's System One models, such as Jev,
|
|
10
|
+
# answer typed questions about a state instead of generating text.
|
|
11
|
+
class Typesafe < Provider
|
|
12
|
+
protocol :system_one, SystemOne
|
|
13
|
+
|
|
14
|
+
def api_base
|
|
15
|
+
@config.typesafe_api_base || 'https://api.typesafe.ai'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def headers
|
|
19
|
+
{ 'Authorization' => "Bearer #{@config.typesafe_api_key}" }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def evaluate(state, questions, model:) # :nodoc:
|
|
23
|
+
resolve_protocol(nil, model).new(self, model).evaluate(state, questions, model: model_id_for(model))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def parse_error(response) # :nodoc:
|
|
27
|
+
body = parse_error_body(response)
|
|
28
|
+
detail = body['detail'] if body.is_a?(Hash)
|
|
29
|
+
|
|
30
|
+
case detail
|
|
31
|
+
when Hash then detail['message'] || super
|
|
32
|
+
when Array then detail.map { |part| detail_message(part) }.join('. ')
|
|
33
|
+
when String then detail
|
|
34
|
+
else super
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
def display_name
|
|
40
|
+
'TypeSafe'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def configuration_options
|
|
44
|
+
%i[typesafe_api_key typesafe_api_base]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def configuration_requirements
|
|
48
|
+
%i[typesafe_api_key]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The model listing returns aliases only, while versioned ids such
|
|
52
|
+
# as jev-1.13.0 are accepted too.
|
|
53
|
+
def assume_models_exist?
|
|
54
|
+
true
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def detail_message(part)
|
|
61
|
+
part.is_a?(Hash) ? part['msg'].to_s : part.to_s
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
RubyLLM::Provider.register :typesafe, RubyLLM::Providers::Typesafe,
|
|
68
|
+
models: File.expand_path('../../../models.json', __dir__)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Typesafe
|
|
5
|
+
# The answers to one Typesafe.evaluate call, keyed by the ids you gave
|
|
6
|
+
# the questions.
|
|
7
|
+
#
|
|
8
|
+
# evaluation[:urgent].probability # => 0.92
|
|
9
|
+
# evaluation.cost.total # => 0.0000131
|
|
10
|
+
#
|
|
11
|
+
class Evaluation
|
|
12
|
+
include Support::Inspectable
|
|
13
|
+
|
|
14
|
+
# The answer to a yes/no question. +probability+ is the chance the
|
|
15
|
+
# answer is yes, from 0 to 1.
|
|
16
|
+
Noul = Struct.new(:probability, keyword_init: true) do
|
|
17
|
+
# Returns whether +probability+ reaches +threshold+. Tune the
|
|
18
|
+
# threshold on your own data and consequences.
|
|
19
|
+
def yes?(threshold: 0.5)
|
|
20
|
+
probability >= threshold
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The answer to a choice question. +option+ is the most probable
|
|
25
|
+
# option, +probabilities+ maps every option to its probability, and
|
|
26
|
+
# +confidence+ (0 to 1) says how concentrated they are.
|
|
27
|
+
Choice = Struct.new(:option, :probabilities, :confidence, keyword_init: true)
|
|
28
|
+
|
|
29
|
+
# The answer to a score question. +score+ is the probability-weighted
|
|
30
|
+
# level index and can land between levels. +probabilities+ holds one
|
|
31
|
+
# probability per level, in the order of +levels+.
|
|
32
|
+
Score = Struct.new(:score, :levels, :probabilities, :confidence, keyword_init: true) do
|
|
33
|
+
# Returns the level description nearest to +score+.
|
|
34
|
+
def level
|
|
35
|
+
levels[score.round]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The answers as a Hash of question id to Noul, Choice, or Score.
|
|
40
|
+
attr_reader :answers
|
|
41
|
+
|
|
42
|
+
# The versioned id of the model that answered, such as "jev-1.13.0".
|
|
43
|
+
attr_reader :model
|
|
44
|
+
|
|
45
|
+
# The raw provider response body.
|
|
46
|
+
attr_reader :raw
|
|
47
|
+
|
|
48
|
+
# The Tokens the evaluation used.
|
|
49
|
+
attr_reader :tokens
|
|
50
|
+
|
|
51
|
+
def initialize(answers:, model:, tokens:, model_info: nil, raw: nil) # :nodoc:
|
|
52
|
+
@answers = answers
|
|
53
|
+
@model = model
|
|
54
|
+
@tokens = tokens
|
|
55
|
+
@model_info = model_info
|
|
56
|
+
@raw = raw
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns the answer to the question with +id+.
|
|
60
|
+
def [](id)
|
|
61
|
+
answers.fetch(id)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Returns the evaluation's Cost, or a Cost with a +nil+ total when the
|
|
65
|
+
# model has no known pricing.
|
|
66
|
+
def cost
|
|
67
|
+
Cost.new(tokens: tokens, model: @model_info)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def inspect_attributes # :nodoc:
|
|
71
|
+
{ model: model, answers: answers.keys }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|