typesafe-ai-rails 0.4.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/.gitignore +5 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +208 -0
- data/Rakefile +12 -0
- data/lib/generators/typesafe/rails/install/install_generator.rb +52 -0
- data/lib/generators/typesafe/rails/install/templates/POST_INSTALL.md +20 -0
- data/lib/generators/typesafe/rails/install/templates/create_typesafe_rails_tables.rb.tt +30 -0
- data/lib/generators/typesafe/rails/install/templates/typesafe.rb.tt +23 -0
- data/lib/generators/typesafe/rails/install/templates/upgrade_typesafe_rails_to_0_4.rb.tt +30 -0
- data/lib/typesafe/rails/call_log.rb +9 -0
- data/lib/typesafe/rails/client.rb +147 -0
- data/lib/typesafe/rails/decision_policy.rb +29 -0
- data/lib/typesafe/rails/questions.rb +63 -0
- data/lib/typesafe/rails/railtie.rb +29 -0
- data/lib/typesafe/rails/result.rb +113 -0
- data/lib/typesafe/rails/version.rb +7 -0
- data/lib/typesafe/rails.rb +29 -0
- data/lib/typesafe-ai-rails.rb +3 -0
- data/typesafe-ai-rails.gemspec +35 -0
- metadata +171 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d274cc3f15ebcb2e9660a74947c1e7487d930aef743b286a3735a046e010d45a
|
|
4
|
+
data.tar.gz: 555561362e7bfe28c3db08b8edd74d7fca31dee5831de921142a8b3dfe456f15
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3e656b5c1000a3dcf37d29303b2c70489a1e6d98205a6d9eb35c84199b43bcb2a1a1f84e0b1c29e26481cd7b98f7893c89fe815600e6693b239feba0af75a221
|
|
7
|
+
data.tar.gz: 8a97bb57ff81e1ccc0fdc398fe13ad4ce41d29a8de38cc7b85d796ef2f46a8038c10f33e5b14688c41f0aec784b32abddbf7b7082236c0ba6ec140c88a595190
|
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
Initial public release under the `typesafe-ai-rails` name.
|
|
6
|
+
|
|
7
|
+
- Fail-closed Choice/Score confidence gating backed by Rails persistence.
|
|
8
|
+
- Answer-specific policies with wildcard defaults and distinct fallback handlers.
|
|
9
|
+
- Explicit Noul handling: probability is never treated as confidence.
|
|
10
|
+
- Forward-compatible plain-hash question helpers with documented Score validation.
|
|
11
|
+
- Direct per-call SDK keyword forwarding.
|
|
12
|
+
- Model-aware, non-fatal call/cost telemetry with pricing snapshots, latency, usage,
|
|
13
|
+
and request IDs.
|
|
14
|
+
- Jev-family pricing handles versioned response model names such as `jev-1.13.0`.
|
|
15
|
+
- SDK logging is opt-in to avoid accidental request/response body logging.
|
|
16
|
+
- Tests use the real community `typesafe-sdk` request/response contract.
|
|
17
|
+
- Fresh-install and development-snapshot upgrade migrations.
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Genie Developments
|
|
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,208 @@
|
|
|
1
|
+
# typesafe-ai-rails
|
|
2
|
+
|
|
3
|
+
Community Rails integration for TypeSafe AI's System One API, built on the
|
|
4
|
+
community [`typesafe-sdk`](https://github.com/joshmn/typesafe-sdk) Ruby gem.
|
|
5
|
+
This project is not an official TypeSafe package.
|
|
6
|
+
|
|
7
|
+
The SDK stays framework-neutral. `typesafe-ai-rails` adds Rails configuration,
|
|
8
|
+
persisted usage/cost telemetry, and an opt-in persistence-backed confidence
|
|
9
|
+
policy for Choice and Score answers.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle add typesafe-ai-rails
|
|
15
|
+
bin/rails generate typesafe:rails:install
|
|
16
|
+
bin/rails db:migrate
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Bundler loads the gem through `typesafe-ai-rails`. The stable Ruby API remains
|
|
20
|
+
under `Typesafe::Rails`; direct users may also `require "typesafe/rails"`.
|
|
21
|
+
|
|
22
|
+
Add the API key to Rails credentials:
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
typesafe:
|
|
26
|
+
api_key: sk-...
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
result = Typesafe::Rails.client.ask(
|
|
33
|
+
decision_type: "support_ticket_routing",
|
|
34
|
+
state: ticket.body,
|
|
35
|
+
questions: {
|
|
36
|
+
department: Typesafe::Rails.choice(
|
|
37
|
+
"Which team should handle this",
|
|
38
|
+
{ billing: nil, technical: nil, sales: nil }
|
|
39
|
+
),
|
|
40
|
+
is_urgent: Typesafe::Rails.noul("Is this time-sensitive?"),
|
|
41
|
+
frustration: Typesafe::Rails.score(
|
|
42
|
+
"How frustrated is the customer",
|
|
43
|
+
["Calm", "Frustrated but civil", "Very angry"]
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`ask` sends all questions about the state in one System One call. That follows
|
|
50
|
+
TypeSafe's recommendation to batch independent questions that share state.
|
|
51
|
+
|
|
52
|
+
The returned `Result` exposes the SDK response directly:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
result[:department]
|
|
56
|
+
result.choices
|
|
57
|
+
result.scores
|
|
58
|
+
result.nouls
|
|
59
|
+
result.usage
|
|
60
|
+
result.response
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Confidence policies
|
|
64
|
+
|
|
65
|
+
TypeSafe returns `confidence` for Choice and Score answers. Noul does **not**
|
|
66
|
+
have a separate confidence value; its `noul` field is the probability of yes.
|
|
67
|
+
|
|
68
|
+
`Result#act!` is a fail-closed helper for Choice/Score side effects. Create
|
|
69
|
+
either a policy for a specific answer or a wildcard policy for the decision:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
Typesafe::Rails::DecisionPolicy.create!(
|
|
73
|
+
decision_type: "support_ticket_routing",
|
|
74
|
+
answer_key: "department",
|
|
75
|
+
confidence_threshold: 0.7,
|
|
76
|
+
fallback: "surface_to_user"
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A wildcard row applies to every confidence-bearing answer that does not have a
|
|
81
|
+
more specific row:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
Typesafe::Rails::DecisionPolicy.create!(
|
|
85
|
+
decision_type: "support_ticket_routing",
|
|
86
|
+
answer_key: "*",
|
|
87
|
+
confidence_threshold: 0.5,
|
|
88
|
+
fallback: "surface_to_user"
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Then gate the side effect:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
result.act!(
|
|
96
|
+
:department,
|
|
97
|
+
fallback: ->(_answer) { route_to_human(ticket) }
|
|
98
|
+
) do |answer|
|
|
99
|
+
ticket.route_to!(answer.choice)
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
If no active policy exists, `act!` raises
|
|
104
|
+
`Typesafe::Rails::MissingPolicyError`. Intentionally ungated reads should use
|
|
105
|
+
`result[:department]` directly.
|
|
106
|
+
|
|
107
|
+
For distinct fallback modes, pass handlers by mode:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
result.act!(
|
|
111
|
+
:department,
|
|
112
|
+
fallback: {
|
|
113
|
+
deterministic_rule: ->(answer) { route_with_rules(ticket, answer) },
|
|
114
|
+
surface_to_user: ->(_answer) { ask_customer(ticket) },
|
|
115
|
+
missing_answer: ->(_answer) { route_to_human(ticket) }
|
|
116
|
+
}
|
|
117
|
+
) do |answer|
|
|
118
|
+
ticket.route_to!(answer.choice)
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`fallback: "escalate"` raises `Typesafe::Rails::LowConfidenceError`.
|
|
123
|
+
|
|
124
|
+
For Noul, threshold its probability directly:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
urgent = result[:is_urgent]
|
|
128
|
+
escalate(ticket) if urgent.noul >= 0.8
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Question helpers
|
|
132
|
+
|
|
133
|
+
`Typesafe::Rails.noul`, `.choice`, and `.score` return plain question hashes.
|
|
134
|
+
The Ruby SDK accepts hashes, so new API fields can pass through before the SDK
|
|
135
|
+
adds matching constructor keywords:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
Typesafe::Rails.noul(
|
|
139
|
+
"Is this relevant?",
|
|
140
|
+
weight: 2,
|
|
141
|
+
future_field: { enabled: true }
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Choice requires a non-empty criteria hash. Score requires the currently
|
|
146
|
+
documented 2–10 ordered levels.
|
|
147
|
+
|
|
148
|
+
You can always use `Typesafe::SDK::Noul`, `Choice`, `Score`, or raw hashes
|
|
149
|
+
directly in the same `questions:` map.
|
|
150
|
+
|
|
151
|
+
## Configuration
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
Rails.application.config.typesafe.api_key =
|
|
155
|
+
Rails.application.credentials.dig(:typesafe, :api_key)
|
|
156
|
+
|
|
157
|
+
Rails.application.config.typesafe.model = "jev-latest"
|
|
158
|
+
Rails.application.config.typesafe.timeout = 10.0
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Client-level SDK options are available through Rails configuration:
|
|
162
|
+
`base_url`, `headers`, `user_agent`, `logger`, `retry_policy`, and `transport`.
|
|
163
|
+
|
|
164
|
+
SDK logging is deliberately opt-in. At debug level, the Ruby SDK logs request
|
|
165
|
+
and response bodies, so enabling it may place application state in logs:
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
Rails.application.config.typesafe.logger = Rails.logger
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Per-call SDK options are forwarded directly:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
Typesafe::Rails.client.ask(
|
|
175
|
+
decision_type: "routing",
|
|
176
|
+
state: ticket.body,
|
|
177
|
+
questions: questions,
|
|
178
|
+
model: "jev-1.12",
|
|
179
|
+
extra_body: { beam_width: 4 }
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
For anything else, `Typesafe::Rails.client.sdk` exposes the underlying client.
|
|
184
|
+
Calls made directly on it bypass Rails telemetry.
|
|
185
|
+
|
|
186
|
+
## Telemetry and pricing
|
|
187
|
+
|
|
188
|
+
Each successful `ask` attempts to append a row to `typesafe_calls` containing:
|
|
189
|
+
|
|
190
|
+
- decision type and returned model
|
|
191
|
+
- input/output token counts
|
|
192
|
+
- the pricing rates used for the estimate
|
|
193
|
+
- estimated USD cost
|
|
194
|
+
- request ID and local latency
|
|
195
|
+
|
|
196
|
+
Pricing is keyed by the returned model. The built-in defaults include a Jev
|
|
197
|
+
family rate so `jev-latest` can resolve to versioned names such as `jev-1.13.0`
|
|
198
|
+
without losing the cost estimate. Unknown model families are recorded with
|
|
199
|
+
`cost_usd = NULL` rather than an invented price. Override `pricing` when
|
|
200
|
+
TypeSafe changes its published rates.
|
|
201
|
+
|
|
202
|
+
A telemetry database failure is non-fatal by default. Set
|
|
203
|
+
`Rails.application.config.typesafe.strict_logging = true` when complete
|
|
204
|
+
accounting is more important than availability.
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module Typesafe
|
|
7
|
+
module Rails
|
|
8
|
+
module Generators
|
|
9
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
10
|
+
include ::Rails::Generators::Migration
|
|
11
|
+
|
|
12
|
+
source_root File.expand_path("templates", __dir__)
|
|
13
|
+
|
|
14
|
+
desc "Installs or upgrades TypeSafe AI Rails integration tables and configuration."
|
|
15
|
+
|
|
16
|
+
def self.next_migration_number(dirname)
|
|
17
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create_migration_files
|
|
21
|
+
if migration_exists?("create_typesafe_rails_tables")
|
|
22
|
+
unless migration_exists?("upgrade_typesafe_rails_to_0_4")
|
|
23
|
+
migration_template(
|
|
24
|
+
"upgrade_typesafe_rails_to_0_4.rb.tt",
|
|
25
|
+
"db/migrate/upgrade_typesafe_rails_to_0_4.rb"
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
else
|
|
29
|
+
migration_template(
|
|
30
|
+
"create_typesafe_rails_tables.rb.tt",
|
|
31
|
+
"db/migrate/create_typesafe_rails_tables.rb"
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def add_initializer
|
|
37
|
+
template "typesafe.rb.tt", "config/initializers/typesafe.rb"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def show_readme
|
|
41
|
+
readme "POST_INSTALL.md" if behavior == :invoke
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def migration_exists?(basename)
|
|
47
|
+
Dir.glob(File.join(destination_root, "db/migrate/*_#{basename}.rb")).any?
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Typesafe::Rails is installed.
|
|
2
|
+
|
|
3
|
+
Next steps:
|
|
4
|
+
|
|
5
|
+
1. Run `bin/rails db:migrate`.
|
|
6
|
+
|
|
7
|
+
2. Add your API key: `bin/rails credentials:edit` and add:
|
|
8
|
+
|
|
9
|
+
typesafe:
|
|
10
|
+
api_key: sk-...
|
|
11
|
+
|
|
12
|
+
3. Call System One through `Typesafe::Rails.client.ask`.
|
|
13
|
+
|
|
14
|
+
4. Before calling `result.act!`, create an active policy for that answer
|
|
15
|
+
key, or a wildcard `answer_key: "*"` policy for the decision type.
|
|
16
|
+
`act!` intentionally fails closed when no policy exists.
|
|
17
|
+
|
|
18
|
+
Noul answers do not have TypeSafe confidence. Read `answer.noul` and apply
|
|
19
|
+
the probability threshold appropriate to your application instead of using
|
|
20
|
+
`act!`.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
class CreateTypesafeRailsTables < ActiveRecord::Migration<%= "[#{ActiveRecord::Migration.current_version}]" %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :typesafe_decision_policies do |t|
|
|
4
|
+
t.string :decision_type, null: false
|
|
5
|
+
t.string :answer_key, null: false, default: "*"
|
|
6
|
+
t.decimal :confidence_threshold, precision: 3, scale: 2, null: false
|
|
7
|
+
t.string :fallback, null: false, default: "surface_to_user"
|
|
8
|
+
t.boolean :active, null: false, default: true
|
|
9
|
+
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
add_index :typesafe_decision_policies, [:decision_type, :answer_key], unique: true
|
|
13
|
+
|
|
14
|
+
create_table :typesafe_calls do |t|
|
|
15
|
+
t.string :decision_type, null: false
|
|
16
|
+
t.string :model
|
|
17
|
+
t.integer :input_tokens
|
|
18
|
+
t.integer :output_tokens
|
|
19
|
+
t.decimal :input_rate, precision: 12, scale: 6
|
|
20
|
+
t.decimal :output_rate, precision: 12, scale: 6
|
|
21
|
+
t.decimal :cost_usd, precision: 12, scale: 6
|
|
22
|
+
t.decimal :latency_ms, precision: 12, scale: 3
|
|
23
|
+
t.string :request_id
|
|
24
|
+
t.datetime :created_at, null: false
|
|
25
|
+
end
|
|
26
|
+
add_index :typesafe_calls, :decision_type
|
|
27
|
+
add_index :typesafe_calls, :created_at
|
|
28
|
+
add_index :typesafe_calls, :request_id
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Rails.application.config.typesafe.api_key =
|
|
4
|
+
Rails.application.credentials.dig(:typesafe, :api_key)
|
|
5
|
+
|
|
6
|
+
Rails.application.config.typesafe.model = "jev-latest"
|
|
7
|
+
Rails.application.config.typesafe.timeout = 10.0
|
|
8
|
+
|
|
9
|
+
# SDK logging is opt-in. Debug logging includes request/response bodies, which
|
|
10
|
+
# may contain sensitive application state.
|
|
11
|
+
# Rails.application.config.typesafe.logger = Rails.logger
|
|
12
|
+
|
|
13
|
+
# A telemetry write failure should not make a successful TypeSafe call fail.
|
|
14
|
+
# Set this true when complete accounting is more important than availability.
|
|
15
|
+
Rails.application.config.typesafe.strict_logging = false
|
|
16
|
+
|
|
17
|
+
# TypeSafe currently prices the Jev family uniformly. Unknown model families
|
|
18
|
+
# are logged with cost_usd = NULL rather than an invented price.
|
|
19
|
+
Rails.application.config.typesafe.pricing = {
|
|
20
|
+
"jev" => { input_per_mtok: 0.042, output_per_mtok: 0.0 },
|
|
21
|
+
"jev-latest" => { input_per_mtok: 0.042, output_per_mtok: 0.0 },
|
|
22
|
+
"jev-1.12" => { input_per_mtok: 0.042, output_per_mtok: 0.0 }
|
|
23
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
class UpgradeTypesafeRailsTo04 < ActiveRecord::Migration<%= "[#{ActiveRecord::Migration.current_version}]" %>
|
|
2
|
+
def up
|
|
3
|
+
unless column_exists?(:typesafe_decision_policies, :answer_key)
|
|
4
|
+
add_column :typesafe_decision_policies, :answer_key, :string, null: false, default: "*"
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
if index_exists?(:typesafe_decision_policies, :decision_type, unique: true)
|
|
8
|
+
remove_index :typesafe_decision_policies, :decision_type
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
unless index_exists?(:typesafe_decision_policies, [:decision_type, :answer_key], unique: true)
|
|
12
|
+
add_index :typesafe_decision_policies, [:decision_type, :answer_key], unique: true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
add_column :typesafe_calls, :model, :string unless column_exists?(:typesafe_calls, :model)
|
|
16
|
+
add_column :typesafe_calls, :input_rate, :decimal,
|
|
17
|
+
precision: 12, scale: 6 unless column_exists?(:typesafe_calls, :input_rate)
|
|
18
|
+
add_column :typesafe_calls, :output_rate, :decimal,
|
|
19
|
+
precision: 12, scale: 6 unless column_exists?(:typesafe_calls, :output_rate)
|
|
20
|
+
add_column :typesafe_calls, :latency_ms, :decimal,
|
|
21
|
+
precision: 12, scale: 3 unless column_exists?(:typesafe_calls, :latency_ms)
|
|
22
|
+
|
|
23
|
+
add_index :typesafe_calls, :request_id unless index_exists?(:typesafe_calls, :request_id)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def down
|
|
27
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
28
|
+
"0.4 allows multiple answer-specific policies per decision_type"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "typesafe/sdk"
|
|
4
|
+
|
|
5
|
+
module Typesafe
|
|
6
|
+
module Rails
|
|
7
|
+
class Client
|
|
8
|
+
SDK_OPTIONS = %i[base_url headers user_agent logger retry_policy transport].freeze
|
|
9
|
+
|
|
10
|
+
DEFAULT_PRICING = {
|
|
11
|
+
"jev" => { input_per_mtok: 0.042, output_per_mtok: 0.0 },
|
|
12
|
+
"jev-latest" => { input_per_mtok: 0.042, output_per_mtok: 0.0 },
|
|
13
|
+
"jev-1.12" => { input_per_mtok: 0.042, output_per_mtok: 0.0 }
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def self.from_config
|
|
17
|
+
config = Typesafe::Rails.configuration
|
|
18
|
+
|
|
19
|
+
new(
|
|
20
|
+
api_key: config&.api_key || ::Rails.application.credentials.dig(:typesafe, :api_key),
|
|
21
|
+
model: config&.model || "jev-latest",
|
|
22
|
+
timeout: config&.timeout || 10.0,
|
|
23
|
+
pricing: config&.pricing || DEFAULT_PRICING,
|
|
24
|
+
strict_logging: config&.strict_logging || false,
|
|
25
|
+
**sdk_options_from(config)
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.sdk_options_from(config)
|
|
30
|
+
SDK_OPTIONS.each_with_object({}) do |key, acc|
|
|
31
|
+
value = config&.public_send(key)
|
|
32
|
+
acc[key] = value unless value.nil?
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
private_class_method :sdk_options_from
|
|
36
|
+
|
|
37
|
+
def initialize(
|
|
38
|
+
api_key:,
|
|
39
|
+
model: "jev-latest",
|
|
40
|
+
timeout: 10.0,
|
|
41
|
+
pricing: DEFAULT_PRICING,
|
|
42
|
+
strict_logging: false,
|
|
43
|
+
sdk_client: nil,
|
|
44
|
+
**sdk_options
|
|
45
|
+
)
|
|
46
|
+
@pricing = pricing
|
|
47
|
+
@strict_logging = strict_logging
|
|
48
|
+
@sdk = sdk_client || Typesafe::SDK::Client.new(
|
|
49
|
+
api_key: api_key, model: model, timeout: timeout, **sdk_options
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def ask(decision_type:, state:, questions:, **call_options)
|
|
54
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
55
|
+
response = @sdk.system_one(state: state, questions: questions, **call_options)
|
|
56
|
+
latency_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0
|
|
57
|
+
|
|
58
|
+
record_call(response, decision_type: decision_type, latency_ms: latency_ms)
|
|
59
|
+
Result.new(response: response, decision_type: decision_type)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def models
|
|
63
|
+
@sdk.models
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def close
|
|
67
|
+
@sdk.close if @sdk.respond_to?(:close)
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
attr_reader :sdk
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def record_call(response, decision_type:, latency_ms:)
|
|
76
|
+
rates = pricing_for(response.model)
|
|
77
|
+
input_rate = rate_value(rates, :input_per_mtok)
|
|
78
|
+
output_rate = rate_value(rates, :output_per_mtok)
|
|
79
|
+
|
|
80
|
+
CallLog.create!(
|
|
81
|
+
decision_type: decision_type.to_s,
|
|
82
|
+
model: response.model,
|
|
83
|
+
input_tokens: response.usage&.input_tokens,
|
|
84
|
+
output_tokens: response.usage&.output_tokens,
|
|
85
|
+
input_rate: input_rate,
|
|
86
|
+
output_rate: output_rate,
|
|
87
|
+
cost_usd: estimate_cost(response.usage, input_rate, output_rate),
|
|
88
|
+
latency_ms: latency_ms,
|
|
89
|
+
request_id: response.respond_to?(:request_id) ? response.request_id : nil
|
|
90
|
+
)
|
|
91
|
+
rescue StandardError => error
|
|
92
|
+
raise if @strict_logging
|
|
93
|
+
|
|
94
|
+
warn_logging_failure(error)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def pricing_for(model)
|
|
98
|
+
return @pricing.call(model) if @pricing.respond_to?(:call)
|
|
99
|
+
return nil unless @pricing.respond_to?(:[])
|
|
100
|
+
return @pricing if rate_hash?(@pricing)
|
|
101
|
+
|
|
102
|
+
exact = @pricing[model] ||
|
|
103
|
+
(model.respond_to?(:to_sym) ? @pricing[model.to_sym] : nil)
|
|
104
|
+
return exact unless exact.nil?
|
|
105
|
+
|
|
106
|
+
if model.to_s.start_with?("jev-")
|
|
107
|
+
family = @pricing["jev"] || @pricing[:jev]
|
|
108
|
+
return family unless family.nil?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
@pricing["default"] || @pricing[:default]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def rate_hash?(value)
|
|
115
|
+
value.is_a?(Hash) &&
|
|
116
|
+
(value.key?(:input_per_mtok) || value.key?("input_per_mtok"))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def rate_value(rates, key)
|
|
120
|
+
return nil unless rates.respond_to?(:[])
|
|
121
|
+
|
|
122
|
+
value = rates[key]
|
|
123
|
+
value = rates[key.to_s] if value.nil?
|
|
124
|
+
value&.to_f
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def estimate_cost(usage, input_rate, output_rate)
|
|
128
|
+
return nil if usage.nil? || input_rate.nil? || output_rate.nil?
|
|
129
|
+
return nil if usage.input_tokens.nil?
|
|
130
|
+
return nil if output_rate.nonzero? && usage.output_tokens.nil?
|
|
131
|
+
|
|
132
|
+
input_cost = usage.input_tokens.to_f / 1_000_000 * input_rate
|
|
133
|
+
output_tokens = usage.output_tokens || 0
|
|
134
|
+
output_cost = output_tokens.to_f / 1_000_000 * output_rate
|
|
135
|
+
input_cost + output_cost
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def warn_logging_failure(error)
|
|
139
|
+
return unless defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
|
|
140
|
+
|
|
141
|
+
::Rails.logger.warn(
|
|
142
|
+
"typesafe-ai-rails could not persist call telemetry: #{error.class}: #{error.message}"
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
module Rails
|
|
5
|
+
class DecisionPolicy < ActiveRecord::Base
|
|
6
|
+
self.table_name = "typesafe_decision_policies"
|
|
7
|
+
|
|
8
|
+
DEFAULT_ANSWER_KEY = "*"
|
|
9
|
+
FALLBACKS = %w[deterministic_rule surface_to_user escalate].freeze
|
|
10
|
+
|
|
11
|
+
validates :decision_type, presence: true
|
|
12
|
+
validates :answer_key, presence: true,
|
|
13
|
+
uniqueness: { scope: :decision_type }
|
|
14
|
+
validates :confidence_threshold,
|
|
15
|
+
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 1 }
|
|
16
|
+
validates :fallback, inclusion: { in: FALLBACKS }
|
|
17
|
+
|
|
18
|
+
scope :active, -> { where(active: true) }
|
|
19
|
+
|
|
20
|
+
def self.resolve(decision_type:, answer_key:)
|
|
21
|
+
decision_type = decision_type.to_s
|
|
22
|
+
answer_key = answer_key.to_s
|
|
23
|
+
|
|
24
|
+
active.find_by(decision_type: decision_type, answer_key: answer_key) ||
|
|
25
|
+
active.find_by(decision_type: decision_type, answer_key: DEFAULT_ANSWER_KEY)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
module Rails
|
|
5
|
+
module Questions
|
|
6
|
+
def noul(instructions = nil, criteria = nil, **fields)
|
|
7
|
+
question_hash("noul", instructions, criteria, fields) do |value|
|
|
8
|
+
next if value.nil? || value.is_a?(Hash)
|
|
9
|
+
|
|
10
|
+
raise ArgumentError, "noul criteria must be a hash with true/false descriptions"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def choice(instructions = nil, criteria = nil, **fields)
|
|
15
|
+
question_hash("choice", instructions, criteria, fields) do |value|
|
|
16
|
+
unless value.is_a?(Hash) && !value.empty?
|
|
17
|
+
raise ArgumentError, "choice requires a non-empty criteria hash"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def score(instructions = nil, criteria = nil, **fields)
|
|
23
|
+
question_hash("score", instructions, criteria, fields) do |value|
|
|
24
|
+
unless value.is_a?(Array) && value.length.between?(2, 10)
|
|
25
|
+
raise ArgumentError, "score criteria must be an array with 2 to 10 levels"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def question_hash(type, instructions, criteria, fields)
|
|
33
|
+
fields = fields.dup
|
|
34
|
+
reject_type_override!(fields, type)
|
|
35
|
+
instructions = merge_argument!(fields, :instructions, instructions)
|
|
36
|
+
criteria = merge_argument!(fields, :criteria, criteria)
|
|
37
|
+
yield(criteria) if block_given?
|
|
38
|
+
|
|
39
|
+
question = { "type" => type }
|
|
40
|
+
question["instructions"] = instructions unless instructions.nil?
|
|
41
|
+
question["criteria"] = criteria unless criteria.nil?
|
|
42
|
+
fields.each { |key, value| question[key.to_s] = value }
|
|
43
|
+
question
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def merge_argument!(fields, name, positional)
|
|
47
|
+
return positional unless fields.key?(name)
|
|
48
|
+
raise ArgumentError, "#{name} given both positionally and as a keyword" unless positional.nil?
|
|
49
|
+
|
|
50
|
+
fields.delete(name)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def reject_type_override!(fields, expected)
|
|
54
|
+
return unless fields.key?(:type)
|
|
55
|
+
|
|
56
|
+
supplied = fields.delete(:type)
|
|
57
|
+
return if supplied.to_s == expected
|
|
58
|
+
|
|
59
|
+
raise ArgumentError, "#{expected} helper cannot build type #{supplied.inspect}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
module Rails
|
|
5
|
+
class Railtie < ::Rails::Railtie
|
|
6
|
+
config.typesafe = ActiveSupport::OrderedOptions.new
|
|
7
|
+
|
|
8
|
+
config.typesafe.api_key = nil
|
|
9
|
+
config.typesafe.base_url = nil
|
|
10
|
+
config.typesafe.model = "jev-latest"
|
|
11
|
+
config.typesafe.timeout = 10.0
|
|
12
|
+
config.typesafe.headers = nil
|
|
13
|
+
config.typesafe.user_agent = nil
|
|
14
|
+
config.typesafe.logger = nil
|
|
15
|
+
config.typesafe.retry_policy = nil
|
|
16
|
+
config.typesafe.transport = nil
|
|
17
|
+
config.typesafe.strict_logging = false
|
|
18
|
+
config.typesafe.pricing = Client::DEFAULT_PRICING.transform_values(&:dup)
|
|
19
|
+
|
|
20
|
+
initializer "typesafe.configure" do |app|
|
|
21
|
+
Typesafe::Rails.configuration = app.config.typesafe
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
generators do
|
|
25
|
+
require "generators/typesafe/rails/install/install_generator"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Typesafe
|
|
4
|
+
module Rails
|
|
5
|
+
class LowConfidenceError < StandardError
|
|
6
|
+
attr_reader :answer_key, :confidence, :policy
|
|
7
|
+
|
|
8
|
+
def initialize(answer_key, confidence, policy)
|
|
9
|
+
@answer_key = answer_key
|
|
10
|
+
@confidence = confidence
|
|
11
|
+
@policy = policy
|
|
12
|
+
super(
|
|
13
|
+
"#{answer_key.inspect} confidence #{confidence.inspect} is below the " \
|
|
14
|
+
"#{policy.confidence_threshold} threshold configured for " \
|
|
15
|
+
"decision_type #{policy.decision_type.inspect}"
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class MissingPolicyError < StandardError
|
|
21
|
+
attr_reader :decision_type, :answer_key
|
|
22
|
+
|
|
23
|
+
def initialize(decision_type, answer_key)
|
|
24
|
+
@decision_type = decision_type
|
|
25
|
+
@answer_key = answer_key
|
|
26
|
+
super(
|
|
27
|
+
"no active TypeSafe decision policy for #{decision_type.inspect} / " \
|
|
28
|
+
"#{answer_key.inspect}; use Result#[] directly for intentionally ungated reads"
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class UnsupportedConfidenceGateError < StandardError
|
|
34
|
+
attr_reader :answer_key, :answer
|
|
35
|
+
|
|
36
|
+
def initialize(answer_key, answer)
|
|
37
|
+
@answer_key = answer_key
|
|
38
|
+
@answer = answer
|
|
39
|
+
super(
|
|
40
|
+
"#{answer_key.inspect} returned #{answer.class}, which has no TypeSafe confidence value; " \
|
|
41
|
+
"Noul answers expose probability as #noul and must be thresholded explicitly"
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class MissingConfidenceError < StandardError
|
|
47
|
+
attr_reader :answer_key, :answer
|
|
48
|
+
|
|
49
|
+
def initialize(answer_key, answer)
|
|
50
|
+
@answer_key = answer_key
|
|
51
|
+
@answer = answer
|
|
52
|
+
super("#{answer_key.inspect} returned a confidence-bearing answer with confidence=nil")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class Result
|
|
57
|
+
attr_reader :response, :decision_type
|
|
58
|
+
|
|
59
|
+
def initialize(response:, decision_type:)
|
|
60
|
+
@response = response
|
|
61
|
+
@decision_type = decision_type.to_s
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def answers = response.answers
|
|
65
|
+
def nouls = response.nouls
|
|
66
|
+
def choices = response.choices
|
|
67
|
+
def scores = response.scores
|
|
68
|
+
def usage = response.usage
|
|
69
|
+
def [](key) = response[key]
|
|
70
|
+
|
|
71
|
+
def act!(answer_key, fallback:)
|
|
72
|
+
answer = response[answer_key]
|
|
73
|
+
return call_fallback(fallback, :missing_answer, nil) if answer.nil?
|
|
74
|
+
|
|
75
|
+
unless answer.respond_to?(:confidence)
|
|
76
|
+
raise UnsupportedConfidenceGateError.new(answer_key, answer)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
confidence = answer.confidence
|
|
80
|
+
raise MissingConfidenceError.new(answer_key, answer) if confidence.nil?
|
|
81
|
+
|
|
82
|
+
policy = DecisionPolicy.resolve(decision_type: decision_type, answer_key: answer_key)
|
|
83
|
+
raise MissingPolicyError.new(decision_type, answer_key) if policy.nil?
|
|
84
|
+
|
|
85
|
+
return yield(answer) if confidence >= policy.confidence_threshold
|
|
86
|
+
|
|
87
|
+
case policy.fallback
|
|
88
|
+
when "escalate"
|
|
89
|
+
raise LowConfidenceError.new(answer_key, confidence, policy)
|
|
90
|
+
else
|
|
91
|
+
call_fallback(fallback, policy.fallback, answer)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def call_fallback(fallback, mode, answer)
|
|
98
|
+
handler =
|
|
99
|
+
if fallback.respond_to?(:call)
|
|
100
|
+
fallback
|
|
101
|
+
elsif fallback.respond_to?(:[])
|
|
102
|
+
fallback[mode.to_sym] || fallback[mode.to_s]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
unless handler.respond_to?(:call)
|
|
106
|
+
raise ArgumentError, "fallback handler for #{mode.inspect} is required"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
handler.call(answer)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "typesafe/rails/version"
|
|
5
|
+
require "typesafe/rails/questions"
|
|
6
|
+
require "typesafe/rails/decision_policy"
|
|
7
|
+
require "typesafe/rails/call_log"
|
|
8
|
+
require "typesafe/rails/result"
|
|
9
|
+
require "typesafe/rails/client"
|
|
10
|
+
require "typesafe/rails/railtie" if defined?(::Rails::Railtie)
|
|
11
|
+
|
|
12
|
+
module Typesafe
|
|
13
|
+
module Rails
|
|
14
|
+
class << self
|
|
15
|
+
include Questions
|
|
16
|
+
|
|
17
|
+
attr_accessor :configuration
|
|
18
|
+
|
|
19
|
+
def client
|
|
20
|
+
@client ||= Client.from_config
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def reset_client!
|
|
24
|
+
@client&.close
|
|
25
|
+
@client = nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/typesafe/rails/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "typesafe-ai-rails"
|
|
7
|
+
spec.version = Typesafe::Rails::VERSION
|
|
8
|
+
spec.authors = ["Genie Developments"]
|
|
9
|
+
|
|
10
|
+
spec.summary = "Community Rails integration for TypeSafe AI System One."
|
|
11
|
+
spec.description = "Rails configuration, model-aware call/cost telemetry, and fail-closed " \
|
|
12
|
+
"confidence policies for TypeSafe Choice and Score judgments on top of " \
|
|
13
|
+
"the community typesafe-sdk Ruby gem. This is not an official TypeSafe package."
|
|
14
|
+
spec.homepage = "https://github.com/GenieRobot/typesafe-ai-rails"
|
|
15
|
+
spec.license = "MIT"
|
|
16
|
+
spec.required_ruby_version = ">= 3.1"
|
|
17
|
+
|
|
18
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
19
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
20
|
+
spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
|
|
21
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
22
|
+
|
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
|
24
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|\.github)/}) }
|
|
25
|
+
end
|
|
26
|
+
spec.require_paths = ["lib"]
|
|
27
|
+
|
|
28
|
+
spec.add_dependency "activerecord", ">= 7.0", "< 9.0"
|
|
29
|
+
spec.add_dependency "railties", ">= 7.0", "< 9.0"
|
|
30
|
+
spec.add_dependency "typesafe-sdk", "~> 0.3"
|
|
31
|
+
|
|
32
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
|
33
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
34
|
+
spec.add_development_dependency "sqlite3", ">= 2.0", "< 3.0"
|
|
35
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: typesafe-ai-rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.4.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Genie Developments
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-16 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activerecord
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.0'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '9.0'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '7.0'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '9.0'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: railties
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.0'
|
|
40
|
+
- - "<"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '9.0'
|
|
43
|
+
type: :runtime
|
|
44
|
+
prerelease: false
|
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '7.0'
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '9.0'
|
|
53
|
+
- !ruby/object:Gem::Dependency
|
|
54
|
+
name: typesafe-sdk
|
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0.3'
|
|
60
|
+
type: :runtime
|
|
61
|
+
prerelease: false
|
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - "~>"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '0.3'
|
|
67
|
+
- !ruby/object:Gem::Dependency
|
|
68
|
+
name: minitest
|
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - "~>"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '5.0'
|
|
74
|
+
type: :development
|
|
75
|
+
prerelease: false
|
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '5.0'
|
|
81
|
+
- !ruby/object:Gem::Dependency
|
|
82
|
+
name: rake
|
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - "~>"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '13.0'
|
|
88
|
+
type: :development
|
|
89
|
+
prerelease: false
|
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - "~>"
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '13.0'
|
|
95
|
+
- !ruby/object:Gem::Dependency
|
|
96
|
+
name: sqlite3
|
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '2.0'
|
|
102
|
+
- - "<"
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: '3.0'
|
|
105
|
+
type: :development
|
|
106
|
+
prerelease: false
|
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ">="
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '2.0'
|
|
112
|
+
- - "<"
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '3.0'
|
|
115
|
+
description: Rails configuration, model-aware call/cost telemetry, and fail-closed
|
|
116
|
+
confidence policies for TypeSafe Choice and Score judgments on top of the community
|
|
117
|
+
typesafe-sdk Ruby gem. This is not an official TypeSafe package.
|
|
118
|
+
email:
|
|
119
|
+
executables: []
|
|
120
|
+
extensions: []
|
|
121
|
+
extra_rdoc_files: []
|
|
122
|
+
files:
|
|
123
|
+
- ".gitignore"
|
|
124
|
+
- CHANGELOG.md
|
|
125
|
+
- Gemfile
|
|
126
|
+
- LICENSE.txt
|
|
127
|
+
- README.md
|
|
128
|
+
- Rakefile
|
|
129
|
+
- lib/generators/typesafe/rails/install/install_generator.rb
|
|
130
|
+
- lib/generators/typesafe/rails/install/templates/POST_INSTALL.md
|
|
131
|
+
- lib/generators/typesafe/rails/install/templates/create_typesafe_rails_tables.rb.tt
|
|
132
|
+
- lib/generators/typesafe/rails/install/templates/typesafe.rb.tt
|
|
133
|
+
- lib/generators/typesafe/rails/install/templates/upgrade_typesafe_rails_to_0_4.rb.tt
|
|
134
|
+
- lib/typesafe-ai-rails.rb
|
|
135
|
+
- lib/typesafe/rails.rb
|
|
136
|
+
- lib/typesafe/rails/call_log.rb
|
|
137
|
+
- lib/typesafe/rails/client.rb
|
|
138
|
+
- lib/typesafe/rails/decision_policy.rb
|
|
139
|
+
- lib/typesafe/rails/questions.rb
|
|
140
|
+
- lib/typesafe/rails/railtie.rb
|
|
141
|
+
- lib/typesafe/rails/result.rb
|
|
142
|
+
- lib/typesafe/rails/version.rb
|
|
143
|
+
- typesafe-ai-rails.gemspec
|
|
144
|
+
homepage: https://github.com/GenieRobot/typesafe-ai-rails
|
|
145
|
+
licenses:
|
|
146
|
+
- MIT
|
|
147
|
+
metadata:
|
|
148
|
+
source_code_uri: https://github.com/GenieRobot/typesafe-ai-rails
|
|
149
|
+
changelog_uri: https://github.com/GenieRobot/typesafe-ai-rails/blob/main/CHANGELOG.md
|
|
150
|
+
bug_tracker_uri: https://github.com/GenieRobot/typesafe-ai-rails/issues
|
|
151
|
+
rubygems_mfa_required: 'true'
|
|
152
|
+
post_install_message:
|
|
153
|
+
rdoc_options: []
|
|
154
|
+
require_paths:
|
|
155
|
+
- lib
|
|
156
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
157
|
+
requirements:
|
|
158
|
+
- - ">="
|
|
159
|
+
- !ruby/object:Gem::Version
|
|
160
|
+
version: '3.1'
|
|
161
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - ">="
|
|
164
|
+
- !ruby/object:Gem::Version
|
|
165
|
+
version: '0'
|
|
166
|
+
requirements: []
|
|
167
|
+
rubygems_version: 3.5.22
|
|
168
|
+
signing_key:
|
|
169
|
+
specification_version: 4
|
|
170
|
+
summary: Community Rails integration for TypeSafe AI System One.
|
|
171
|
+
test_files: []
|