omniai-evals 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.
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module OmniAI
6
+ module Evals
7
+ # Dates are compared PARSED, never as strings: "2019-03-14", "03/14/2019" and
8
+ # "March 14, 2019" are the same day, and a string compare scores three different
9
+ # transcriptions of one correct answer as two failures.
10
+ module Dates
11
+ # Numeric slash AND dash dates are read as US month-first (MM/DD/YYYY, MM-DD-YYYY),
12
+ # NOT by Date.parse.
13
+ #
14
+ # This is not a style preference, it is a correctness fix. Ruby's Date.parse reads
15
+ # a slash date as DD/MM/YYYY, so it returns nil for "03/14/2019" (there is no month
16
+ # 14) and -- far worse -- silently returns February 4th for "04/02/2019". A judge
17
+ # would then mark a correct April 2nd transcription as wrong, with no error anywhere
18
+ # to notice. Wrong-but-plausible is the failure mode this whole gem exists to catch,
19
+ # so it cannot live in the date comparison itself.
20
+ #
21
+ # STATED DEFAULT (docs/contract.md): every dataset this grades is US, so month-first
22
+ # is the rule. A non-US dataset needs a per-dataset locale field in the contract --
23
+ # it is not something to infer from the text, because "04/02/2019" is valid in both
24
+ # readings and guessing reintroduces exactly the silent error above. An unambiguous
25
+ # key should be written ISO in the criteria file regardless.
26
+ US_NUMERIC = %r{\A(\d{1,2})[/-](\d{1,2})[/-](\d{2,4})\z}
27
+
28
+ # ISO stays ISO: "2019-03-14" is unambiguous and must NOT hit the US rule.
29
+ ISO = /\A\d{4}-\d{2}-\d{2}\z/
30
+ # "2019/03/14" is year-first by shape, so it is unambiguous too.
31
+ ISO_SLASH = %r{\A(\d{4})/(\d{1,2})/(\d{1,2})\z}
32
+
33
+ MONTH = "January|February|March|April|May|June|July|August|September|October|November|December|" \
34
+ "Jan|Feb|Mar|Apr|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec"
35
+
36
+ # Every date shape seen in the faxed medical records this grades. Order matters:
37
+ # the alternation is tried left to right, so longer textual forms come before the
38
+ # numeric ones that could match a prefix of them.
39
+ TOKEN = Regexp.union(
40
+ /(?:#{MONTH})\.?\s+\d{1,2},?\s+\d{4}/, # March 14, 2019 / Mar. 14 2019 / Mar 14 2019
41
+ /\d{1,2}\s+(?:#{MONTH})\.?,?\s+\d{4}/, # 14 March 2019
42
+ %r{\d{4}[-/]\d{1,2}[-/]\d{1,2}}, # 2019-03-14 / 2019/03/14
43
+ %r{\d{1,2}[/-]\d{1,2}[/-]\d{2,4}} # 03/14/2019 / 03-14-2019 / 3/14/19
44
+ )
45
+
46
+ module_function
47
+
48
+ # @return [Date, nil] nil when the value is not a date
49
+ def parse(value)
50
+ return nil if value.nil?
51
+
52
+ text = value.to_s.strip
53
+ return iso(text) if ISO.match?(text)
54
+
55
+ slash = ISO_SLASH.match(text)
56
+ return ymd(slash[1], slash[2], slash[3]) if slash
57
+
58
+ us = US_NUMERIC.match(text)
59
+ return us_numeric(us) if us
60
+
61
+ textual(text)
62
+ end
63
+
64
+ # @return [Array<Date>] every date-shaped token in a blob of text, parsed. Used by
65
+ # the presence assertion so a date key matches whatever format the model wrote.
66
+ def scan(text)
67
+ text.to_s.scan(TOKEN).filter_map { |token| parse(token) }
68
+ end
69
+
70
+ # A textual month ("Mar. 14, 2019", "14 March 2019") is unambiguous, so Date.parse
71
+ # is safe here -- the day-first trap only applies to all-numeric forms.
72
+ def textual(text)
73
+ Date.parse(text)
74
+ rescue Date::Error
75
+ nil
76
+ end
77
+
78
+ def iso(text)
79
+ Date.iso8601(text)
80
+ rescue Date::Error
81
+ nil
82
+ end
83
+
84
+ def us_numeric(match)
85
+ month, day, year = match.captures
86
+ year = expand_year(year)
87
+ ymd(year, month, day)
88
+ end
89
+
90
+ # A two-digit year in a medical record is this century unless that would be absurd;
91
+ # 70 is the conventional pivot and matches Ruby's own strptime behaviour.
92
+ def expand_year(year)
93
+ return year if year.length == 4
94
+
95
+ year.to_i < 70 ? "20#{year}" : "19#{year}"
96
+ end
97
+
98
+ def ymd(year, month, day)
99
+ Date.new(year.to_i, month.to_i, day.to_i)
100
+ rescue Date::Error
101
+ nil
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Evals
5
+ # Base for every error the gem raises.
6
+ class Error < StandardError; end
7
+
8
+ # A file is not the contract version this gem speaks, or is missing a required field.
9
+ class ContractError < Error; end
10
+
11
+ # Two runs were compared whose comparability keys do not match.
12
+ class ComparabilityError < Error; end
13
+
14
+ # An adapter was used without being required. The core loads on stdlib alone, so
15
+ # "default adapter" is a documentation claim, not an implicit require: naming the
16
+ # missing require is the whole point of this error.
17
+ class AdapterNotLoaded < Error; end
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Evals
5
+ # One unit under test. `input` is the section 3.1 input object ("kind" is
6
+ # image/text/messages); `target` is the path to this example's criteria file,
7
+ # relative to the manifest.
8
+ Example = Data.define(:id, :input, :target, :metadata)
9
+ end
10
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Evals
5
+ # The shape of a judge criterion: one boolean field plus a short reason.
6
+ #
7
+ # `packet` takes key_text and output and NOTHING else. That is the enforcement of
8
+ # "the judge receives the key text and the output, never the example input" -- there
9
+ # is no parameter through which the input could arrive, so no caller can leak it.
10
+ #
11
+ # The schema is a plain JSON Schema Hash, not an omniai type: the core loads on
12
+ # stdlib alone (invariant 3), and both adapters want raw JSON Schema anyway --
13
+ # ClaudeCLI passes it to --json-schema, and Adapters::OmniAI deserializes it.
14
+ module Judge
15
+ module_function
16
+
17
+ def schema
18
+ {
19
+ type: "object",
20
+ properties: {
21
+ passed: { type: "boolean", description: "true when the output is faithful to the key" },
22
+ reason: { type: "string", description: "one sentence, under 200 characters" },
23
+ },
24
+ required: %i[passed reason],
25
+ }
26
+ end
27
+
28
+ def packet(key_text:, output:)
29
+ <<~PACKET
30
+ KEY (human-authored ground truth):
31
+ #{key_text}
32
+
33
+ OUTPUT (model under test):
34
+ #{output}
35
+ PACKET
36
+ end
37
+
38
+ # @raise [Error] when the verdict's score is not a boolean. A lax provider can
39
+ # return 0.8 or "true"; the raw ArgumentError from Result names the class but not
40
+ # the criterion, which is useless in a run over hundreds of them.
41
+ def result(verdict, criterion:)
42
+ Result.new(key: criterion.id, score: verdict["passed"], comment: verdict["reason"].to_s)
43
+ rescue ArgumentError => e
44
+ raise Error, "judge verdict for #{criterion.id}: #{e.message}"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module OmniAI
6
+ module Evals
7
+ # The run manifest written beside the results.
8
+ #
9
+ # The first five fields match the first consumer's existing run manifest, in the
10
+ # same shape, so its port is a rename rather than a rewrite. Invariant 2 applies:
11
+ # nothing here may carry input or output text -- argv is sanitized on the way in.
12
+ module Manifest
13
+ REDACTED = "<redacted>"
14
+
15
+ module_function
16
+
17
+ def build(tool:, repo_root:, run_id:, dataset:, criteria:, target:, model:, epochs:,
18
+ argv: ARGV, now: Time.now.utc, params: {}, seed: "none", reducer: "all_epochs",
19
+ gems: {}, judge: nil)
20
+ {
21
+ "contract" => CONTRACT_VERSION,
22
+ "run_id" => run_id,
23
+ "tool" => tool,
24
+ "argv_sanitized" => sanitize_argv(argv),
25
+ "toolkit_sha" => toolkit_sha(repo_root),
26
+ "ruby_version" => RUBY_VERSION,
27
+ "ts" => now.strftime("%Y-%m-%dT%H:%M:%SZ"),
28
+ "dataset" => dataset,
29
+ "criteria" => criteria,
30
+ "target" => target,
31
+ "model" => model,
32
+ "params" => params,
33
+ "seed" => seed,
34
+ "epochs" => epochs,
35
+ "reducer" => reducer,
36
+ "gems" => gems,
37
+ "judge" => judge,
38
+ "comparability_key" => Compare.key(dataset_sha: dataset.fetch("sha256"),
39
+ criteria_sha: criteria.fetch("sha256"),
40
+ prompt_version: target.fetch("prompt_version")),
41
+ }.compact
42
+ end
43
+
44
+ # Redacts the value following --claimant (both "--claimant X" and "--claimant=X"
45
+ # forms) so the manifest never records a claimant name; all other args pass through.
46
+ def sanitize_argv(argv)
47
+ redact_next = false
48
+ argv.map do |arg|
49
+ if redact_next
50
+ redact_next = false
51
+ REDACTED
52
+ elsif arg == "--claimant"
53
+ redact_next = true
54
+ arg
55
+ elsif arg.start_with?("--claimant=")
56
+ "--claimant=#{REDACTED}"
57
+ else
58
+ arg
59
+ end
60
+ end
61
+ end
62
+
63
+ # The toolkit's short git SHA for provenance; "unknown" when repo_root isn't a repo
64
+ # or git is unavailable (never fails the run over provenance).
65
+ def toolkit_sha(repo_root)
66
+ stdout, _stderr, status = Open3.capture3("git", "-C", repo_root, "rev-parse", "--short", "HEAD")
67
+ status.success? ? stdout.strip : "unknown"
68
+ rescue Errno::ENOENT
69
+ "unknown"
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Evals
5
+ # Text tidying shared by the assertions. Kept in one place so "does the output
6
+ # contain this" means the same thing in every assertion.
7
+ module Normalize
8
+ module_function
9
+
10
+ # Output may be a String or the Hash a structured target returned.
11
+ def text(output) = output.is_a?(String) ? output : JSON.generate(output)
12
+
13
+ # Collapse whitespace runs; keep the case.
14
+ def squish(value) = value.to_s.gsub(/\s+/, " ").strip
15
+
16
+ # Collapse whitespace runs AND case -- the comparison form for contains-checks.
17
+ def phrase(value) = squish(value).downcase
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Evals
5
+ # What a run WOULD do: examples x configs x epochs, estimated calls and cost, and
6
+ # zero model calls.
7
+ #
8
+ # `adapter:` is accepted and deliberately never invoked -- the spec passes one that
9
+ # raises, so invariant 5 is enforced by the signature rather than by convention.
10
+ # Prices come from the consumer: the gem knows nothing about any provider's rates.
11
+ #
12
+ # `configs:` is a PLANNING dimension only. A run is one target version, so each
13
+ # config is its own run with its own manifest and its own results file -- this
14
+ # exists so you can price "default vs effort:low" in one go BEFORE committing the
15
+ # spend, not so two configs share a results file. Results.write refuses to mix
16
+ # run_ids in one file, which is what actually prevents the merge.
17
+ module Plan
18
+ module_function
19
+
20
+ def build(dataset:, criteria:, configs:, epochs:, prices:, adapter: nil)
21
+ examples = dataset.examples.size
22
+ judge_criteria = criteria.all.count { |criterion| criterion.scorer == "judge" }
23
+ target_calls = examples * configs.size * epochs
24
+ judge_calls = judge_criteria * configs.size * epochs
25
+
26
+ {
27
+ "examples" => examples,
28
+ "configs" => configs.size,
29
+ "epochs" => epochs,
30
+ "judge_criteria" => judge_criteria,
31
+ "calls" => target_calls + judge_calls,
32
+ "target_calls" => target_calls,
33
+ "judge_calls" => judge_calls,
34
+ "target_cost_usd" => cost(target_calls, prices["target"]),
35
+ "judge_cost_usd" => cost(judge_calls, prices["judge"]),
36
+ "cost_usd" => cost(target_calls, prices["target"]) + cost(judge_calls, prices["judge"]),
37
+ }
38
+ end
39
+
40
+ # The consumer supplies the price table; an absent price is 0.0 rather than an
41
+ # error, because a bench of pure assertions has no metered call to price.
42
+ def cost(calls, price) = (calls * (price || 0.0)).round(4)
43
+
44
+ def render(plan)
45
+ format("%<examples>d examples x %<configs>d configs x %<epochs>d epochs\n" \
46
+ "%<target_calls>d target calls, estimated $%<target_cost>.2f\n" \
47
+ "%<judge_calls>d judge calls, estimated $%<judge_cost>.2f\n" \
48
+ "%<calls>d calls total, estimated $%<cost>.2f\n",
49
+ examples: plan.fetch("examples"), configs: plan.fetch("configs"),
50
+ epochs: plan.fetch("epochs"), calls: plan.fetch("calls"),
51
+ target_calls: plan.fetch("target_calls"), target_cost: plan.fetch("target_cost_usd"),
52
+ judge_calls: plan.fetch("judge_calls"), judge_cost: plan.fetch("judge_cost_usd"),
53
+ cost: plan.fetch("cost_usd"))
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Evals
5
+ # One line of the results file: (example, criterion, epoch).
6
+ #
7
+ # `key`/`score`/`comment` are the LangSmith / Braintrust / openevals record and the
8
+ # only required fields; the rest are ours. THIS FILE IS PHI-BEARING -- a judge comment
9
+ # may quote output -- so the consumer stores it under its own protected path. The
10
+ # scorecard never carries comments.
11
+ module Record
12
+ # docs/contract.md section 2: a scorer is an assertion, a judge, or a label. A typo
13
+ # here would otherwise reach the scorecard as its own silent family.
14
+ SCORERS = %w[assertion judge label].freeze
15
+
16
+ module_function
17
+
18
+ def build(result:, run_id:, example_id:, family:, scorer:, epoch:, cost_usd: 0.0, latency_ms: 0)
19
+ raise ArgumentError, "unknown scorer #{scorer.inspect}" unless SCORERS.include?(scorer.to_s)
20
+
21
+ {
22
+ "contract" => CONTRACT_VERSION,
23
+ "run_id" => run_id,
24
+ "example_id" => example_id,
25
+ "key" => result.key,
26
+ "score" => result.score,
27
+ "comment" => result.comment,
28
+ "family" => family,
29
+ "scorer" => scorer,
30
+ "epoch" => epoch,
31
+ "cost_usd" => cost_usd,
32
+ "latency_ms" => latency_ms,
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Evals
5
+ # How the epochs of one criterion combine into one verdict.
6
+ #
7
+ # `all_epochs` is the default and the only reducer in v1. FLAKY is a first-class
8
+ # verdict rather than a rounding decision: a criterion that passes 3 of 5 times is
9
+ # not "60% correct", it is unreliable, and the reader needs to see that.
10
+ module Reducer
11
+ module_function
12
+
13
+ # @param results [Array<Result>] one per epoch, for a single criterion
14
+ # @return [Symbol] :pass, :fail, or :flaky
15
+ def all_epochs(results)
16
+ raise ArgumentError, "no epochs to reduce" if results.empty?
17
+
18
+ scores = results.map(&:score)
19
+ return :pass if scores.all?
20
+ return :fail if scores.none?
21
+
22
+ :flaky
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Evals
5
+ # One graded criterion. `key`/`score`/`comment` are the LangSmith / Braintrust /
6
+ # openevals record and the only required fields in a result line.
7
+ #
8
+ # The boolean check is here, at the single point every scorer funnels through, so
9
+ # invariant 1 ("no float ever reaches a result record") cannot be violated by a
10
+ # future scorer that returns a confidence. Data is frozen and setter-free, so a
11
+ # validated score is the score that gets written.
12
+ Result = Data.define(:key, :score, :comment) do
13
+ def initialize(key:, score:, comment:)
14
+ unless score.is_a?(TrueClass) || score.is_a?(FalseClass)
15
+ raise ArgumentError, "score must be true or false, got #{score.class}"
16
+ end
17
+
18
+ super(key: key.to_s, score:, comment: comment.to_s)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Evals
5
+ # The JSONL results file.
6
+ module Results
7
+ module_function
8
+
9
+ # Appends rather than truncates: a run writes epoch by epoch, and losing the
10
+ # earlier epochs to a reopen would silently change every reduced verdict.
11
+ #
12
+ # Appending is therefore also the hazard. Re-running a runner into a path that
13
+ # already holds another run doubles every epoch, and the reducer sees 2n epochs
14
+ # without anything raising -- a PASS built from two runs it was never asked to
15
+ # combine. So a file holds exactly one run_id, and a second one is refused.
16
+ #
17
+ # @param records [Array<Hash>]
18
+ # @raise [Error] when the records do not belong to the file's existing run
19
+ def write(path, records)
20
+ return if records.empty?
21
+
22
+ guard_run_id!(path, records)
23
+ File.open(path, "a") do |file|
24
+ records.each { |record| file.puts(JSON.generate(record)) }
25
+ end
26
+ end
27
+
28
+ def guard_run_id!(path, records)
29
+ incoming = records.map { |record| record.fetch("run_id") }.uniq
30
+ raise Error, "records span #{incoming.size} run_ids: #{incoming.join(', ')}" unless incoming.size == 1
31
+ return unless File.exist?(path) && !File.empty?(path)
32
+
33
+ existing = JSON.parse(File.foreach(path).first).fetch("run_id")
34
+ return if existing == incoming.first
35
+
36
+ raise Error, "#{path} holds run #{existing}; refusing to append run #{incoming.first}"
37
+ end
38
+
39
+ # @raise [ContractError]
40
+ # @return [Array<Hash>]
41
+ def read(path)
42
+ File.readlines(path).reject { |line| line.strip.empty? }.map.with_index(1) do |line, number|
43
+ Contract.check!(JSON.parse(line), path: "#{path}:#{number}")
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Evals
5
+ # The PHI-free aggregate: counts, all-pass on top, family rates beneath.
6
+ #
7
+ # Counts, not percentages -- small datasets make rates misleading and the reader can
8
+ # divide. Invariant 2 is enforced by construction: this builds a fresh Hash of
9
+ # integers from the records rather than filtering fields out of them, so there is no
10
+ # path by which a comment reaches the scorecard.
11
+ module Scorecard
12
+ module_function
13
+
14
+ def build(records:, run_id:, comparability_key:, cost_usd: nil, calls: nil)
15
+ by_key = records.group_by { |record| record.fetch("key") }
16
+ verdicts = by_key.transform_values { |rows| reduce(rows) }
17
+
18
+ {
19
+ "contract" => CONTRACT_VERSION,
20
+ "run_id" => run_id,
21
+ "comparability_key" => comparability_key,
22
+ "examples" => examples_for(by_key, verdicts),
23
+ "families" => families_for(by_key, verdicts),
24
+ "cost_usd" => cost_usd,
25
+ "calls" => calls,
26
+ }
27
+ end
28
+
29
+ # Comments are dropped here, not carried and filtered later: the Result rebuilt for
30
+ # the reducer takes an empty comment, so PHI has no route into this method's output.
31
+ def reduce(rows)
32
+ Reducer.all_epochs(rows.map { |row| Result.new(key: row.fetch("key"), score: row.fetch("score"), comment: "") })
33
+ end
34
+
35
+ # An example is only all_pass when every one of its criteria passed; a single FAIL
36
+ # makes it failed, otherwise a FLAKY makes it flaky.
37
+ def examples_for(by_key, verdicts)
38
+ by_example = Hash.new { |hash, key| hash[key] = [] }
39
+ by_key.each { |key, rows| by_example[rows.first.fetch("example_id")] << verdicts.fetch(key) }
40
+
41
+ counts = { "total" => by_example.size, "all_pass" => 0, "flaky" => 0, "failed" => 0 }
42
+ by_example.each_value { |list| counts[bucket(list)] += 1 }
43
+ counts
44
+ end
45
+
46
+ def bucket(verdicts)
47
+ return "failed" if verdicts.include?(:fail)
48
+ return "flaky" if verdicts.include?(:flaky)
49
+
50
+ "all_pass"
51
+ end
52
+
53
+ def families_for(by_key, verdicts)
54
+ by_key.each_with_object({}) do |(key, rows), acc|
55
+ family = rows.first.fetch("family")
56
+ acc[family] ||= { "criteria" => 0, "pass" => 0, "flaky" => 0, "fail" => 0 }
57
+ acc[family]["criteria"] += 1
58
+ acc[family][verdicts.fetch(key).to_s] += 1
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module Evals
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "evals/version"
6
+ require_relative "evals/errors"
7
+ require_relative "evals/contract"
8
+ require_relative "evals/result"
9
+ require_relative "evals/example"
10
+ require_relative "evals/dataset"
11
+ require_relative "evals/criterion"
12
+ require_relative "evals/criteria"
13
+ require_relative "evals/normalize"
14
+ require_relative "evals/dates"
15
+ require_relative "evals/assertions"
16
+ require_relative "evals/reducer"
17
+ require_relative "evals/record"
18
+ require_relative "evals/results"
19
+ require_relative "evals/compare"
20
+ require_relative "evals/manifest"
21
+ require_relative "evals/scorecard"
22
+ require_relative "evals/plan"
23
+ require_relative "evals/judge"
24
+
25
+ # Structure and grading for LLM evals.
26
+ #
27
+ # The core loads with stdlib and `json` only. Judge adapters reach a model and are
28
+ # therefore NOT required here -- a consumer requires the one it wants:
29
+ #
30
+ # require "omniai/evals"
31
+ # require "omniai/evals/adapters/omniai"
32
+ #
33
+ module OmniAI
34
+ # Structure and grading for LLM evals: datasets, boolean criteria, assertions,
35
+ # judges, reducers, and PHI-free scorecards.
36
+ module Evals
37
+ end
38
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omniai-evals
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dean Singh
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: omniai
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '3.8'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '3.8'
26
+ description: Datasets, boolean criteria, assertions, judges, reducers, and PHI-free
27
+ scorecards. A companion to the omniai gem family.
28
+ email:
29
+ - dean.singh@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - CHANGELOG.md
35
+ - Gemfile
36
+ - LICENSE
37
+ - README.md
38
+ - lib/omniai/evals.rb
39
+ - lib/omniai/evals/adapters/call_record.rb
40
+ - lib/omniai/evals/adapters/claude_cli.rb
41
+ - lib/omniai/evals/adapters/omniai.rb
42
+ - lib/omniai/evals/assertions.rb
43
+ - lib/omniai/evals/assertions/absence.rb
44
+ - lib/omniai/evals/assertions/abstention.rb
45
+ - lib/omniai/evals/assertions/circled.rb
46
+ - lib/omniai/evals/assertions/identity.rb
47
+ - lib/omniai/evals/assertions/presence.rb
48
+ - lib/omniai/evals/assertions/structure.rb
49
+ - lib/omniai/evals/compare.rb
50
+ - lib/omniai/evals/contract.rb
51
+ - lib/omniai/evals/criteria.rb
52
+ - lib/omniai/evals/criterion.rb
53
+ - lib/omniai/evals/dataset.rb
54
+ - lib/omniai/evals/dates.rb
55
+ - lib/omniai/evals/errors.rb
56
+ - lib/omniai/evals/example.rb
57
+ - lib/omniai/evals/judge.rb
58
+ - lib/omniai/evals/manifest.rb
59
+ - lib/omniai/evals/normalize.rb
60
+ - lib/omniai/evals/plan.rb
61
+ - lib/omniai/evals/record.rb
62
+ - lib/omniai/evals/reducer.rb
63
+ - lib/omniai/evals/result.rb
64
+ - lib/omniai/evals/results.rb
65
+ - lib/omniai/evals/scorecard.rb
66
+ - lib/omniai/evals/version.rb
67
+ homepage: https://github.com/DeanSingh/omniai-evals
68
+ licenses:
69
+ - MIT
70
+ metadata:
71
+ homepage_uri: https://github.com/DeanSingh/omniai-evals
72
+ changelog_uri: https://github.com/DeanSingh/omniai-evals/blob/main/CHANGELOG.md
73
+ rubygems_mfa_required: 'true'
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 3.2.0
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubygems_version: 3.6.9
89
+ specification_version: 4
90
+ summary: Structure and grading for LLM evals
91
+ test_files: []