inquirex-llm 0.12.1 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +5 -5
- data/lib/inquirex/llm/evals/adapter.rb +9 -9
- data/lib/inquirex/llm/evals/dataset.rb +2 -2
- data/lib/inquirex/llm/evals/model_adapter.rb +3 -3
- data/lib/inquirex/llm/evals/scorer.rb +14 -14
- data/lib/inquirex/llm/evals.rb +6 -6
- data/lib/inquirex/llm/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1261cf90cb18c935731001fc135e896e271d63e64f84aba6fc5b45105bfcaa8c
|
|
4
|
+
data.tar.gz: 74ddcc8813674ae65e5979a3becc1c9ecbd7bdf4b0e64e71161a6ddc15c86a01
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1eda2a5ae132b1027700dc058f90ff47fe3a29e5e8804669b5ad4c9c1771f76b8c01484963e8f3d63d5e3fc9f3311e568d67d73b0f35f7e3cff9c1723a4d723c
|
|
7
|
+
data.tar.gz: 7af624fbd2546c4cf1404f3ac51aecc72c670e013888fb7fdb654be5f2f3ee455023e08737dfea510295bdfa9fd783c4ffe801f2e383004f8491ed463ad02955
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.2 (unreleased)
|
|
4
|
+
|
|
5
|
+
- **The eval suite now runs on llm-evals.** `inquirex/llm/evals` requires `llm/evals` and subclasses `::LLM::Evals`, because `agent-evals` was renamed and is no longer on RubyGems. The config file is `evals/.llm-evals.config.yml` and the executable is `evals`. Inside `Inquirex::LLM`, a bare `LLM` means this gem, so the leading `::` is required.
|
|
6
|
+
|
|
3
7
|
## 0.12.0 (unreleased)
|
|
4
8
|
|
|
5
9
|
Hand-written until `github_changelog_generator` regenerates this file at release. The family-wide notes are in inquirex's `INQUIREX_VERSION_LOG.md`.
|
data/README.md
CHANGED
|
@@ -487,15 +487,15 @@ end
|
|
|
487
487
|
|
|
488
488
|
## Evals
|
|
489
489
|
|
|
490
|
-
Which model extracts best, measured with this gem's own prompts: ten flows, a complete and a vague text each, run on [
|
|
490
|
+
Which model extracts best, measured with this gem's own prompts: ten flows, a complete and a vague text each, run on [llm-evals](https://github.com/kigster/llm-evals). The lineup and the data live in [`evals/`](evals/README.md); the adapter and the scorer are `Inquirex::LLM::Evals`, loaded only by `require "inquirex/llm/evals"`.
|
|
491
491
|
|
|
492
492
|
How a run works:
|
|
493
493
|
|
|
494
494
|
1. **Verify** (offline): each dataset's `schema.rb` must match the `extract` step in its `dsl.rb`, and every `expected.json` must fit that schema.
|
|
495
|
-
1. **Expand the lineup**: every model under `vendors:` in `evals/.
|
|
495
|
+
1. **Expand the lineup**: every model under `vendors:` in `evals/.llm-evals.config.yml`, times each `effort`/`temperature` it lists, becomes one contender. Each contender runs every case.
|
|
496
496
|
1. **Call the model**: one OpenRouter chat completion per case, built from this gem's production prompt and the step's schema.
|
|
497
497
|
1. **Score the raw reply** field by field: `correct` 1.0, `missed` 0.2, `hallucinated` 0.15, `incorrect` 0.1, `invalid` 0. A case's score is the mean over its fields. The rules are in [`evals/README.md`](evals/README.md#scoring).
|
|
498
|
-
1. **Report**: a leaderboard on screen and a JSON log under `log/
|
|
498
|
+
1. **Report**: a leaderboard on screen and a JSON log under `log/llm-evals/`. If `BRAINTRUST_API_KEY` or `LOGFIRE_TOKEN` is set, each case is also sent to that one tracker; `--local` turns that off.
|
|
499
499
|
|
|
500
500
|
```bash
|
|
501
501
|
just evals -n # the whole matrix, calling nothing
|
|
@@ -503,8 +503,8 @@ just evals # every configured model on every dataset
|
|
|
503
503
|
just evals --local -d 05-managed-it-services # one dataset, no remote tracker
|
|
504
504
|
just evals-verify # step 1 only
|
|
505
505
|
|
|
506
|
-
bundle exec
|
|
507
|
-
bundle exec
|
|
506
|
+
bundle exec evals cases # the 20 cases
|
|
507
|
+
bundle exec evals run -m openai/gpt-4o-mini # one model, per-case breakdown
|
|
508
508
|
```
|
|
509
509
|
|
|
510
510
|
`just evals` needs `OPENROUTER_API_KEY` and the `:evals` bundle group. It is a paid run: 14 contenders × 20 cases = 280 calls.
|
|
@@ -9,9 +9,9 @@ module Inquirex
|
|
|
9
9
|
# canonicalized by the gem. It never scores; {Scorer} does that.
|
|
10
10
|
#
|
|
11
11
|
# A reply that is not a JSON object is the model's failure and comes
|
|
12
|
-
# back as a {
|
|
12
|
+
# back as a {LLM::Evals::Reply} with `error` set. A transport failure
|
|
13
13
|
# raises, and the run counts it as an error rather than a wrong answer.
|
|
14
|
-
class Adapter <
|
|
14
|
+
class Adapter < ::LLM::Evals::Adapter
|
|
15
15
|
# Recorded with every experiment: which prompt the models saw.
|
|
16
16
|
PROMPT = "Inquirex::LLM::OpenAIAdapter"
|
|
17
17
|
|
|
@@ -24,7 +24,7 @@ module Inquirex
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# @return [String]
|
|
27
|
-
def root = @root ||
|
|
27
|
+
def root = @root || ::LLM::Evals.configuration.root
|
|
28
28
|
|
|
29
29
|
# Loads the dataset's flow and schema once, before its cases run in threads.
|
|
30
30
|
#
|
|
@@ -36,14 +36,14 @@ module Inquirex
|
|
|
36
36
|
set.schema
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
# @param kase [
|
|
40
|
-
# @param model [
|
|
41
|
-
# @return [
|
|
39
|
+
# @param kase [LLM::Evals::Case]
|
|
40
|
+
# @param model [LLM::Evals::Model]
|
|
41
|
+
# @return [LLM::Evals::Reply]
|
|
42
42
|
def call(kase, model:)
|
|
43
43
|
set = dataset(kase.dataset)
|
|
44
44
|
adapter = ModelAdapter.new(model)
|
|
45
45
|
extraction = adapter.call(set.node, { set.source_step => kase.input })
|
|
46
|
-
|
|
46
|
+
::LLM::Evals::Reply.new(
|
|
47
47
|
output: extraction.output.transform_keys(&:to_s),
|
|
48
48
|
raw: extraction.raw.transform_keys(&:to_s),
|
|
49
49
|
usage: adapter.completion.usage,
|
|
@@ -52,13 +52,13 @@ module Inquirex
|
|
|
52
52
|
rescue Errors::AdapterError => e
|
|
53
53
|
raise if adapter.completion.nil? # nothing was asked: not the model's doing
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
::LLM::Evals::Reply.new(error: e.message.lines.first.strip,
|
|
56
56
|
raw: adapter.completion.text,
|
|
57
57
|
usage: adapter.completion.usage,
|
|
58
58
|
metadata: { prompt: adapter.prompt })
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
# @param kase [
|
|
61
|
+
# @param kase [LLM::Evals::Case]
|
|
62
62
|
# @return [Hash] the schema the scorer grades against
|
|
63
63
|
def context(kase) = { schema: dataset(kase.dataset).schema }
|
|
64
64
|
|
|
@@ -6,7 +6,7 @@ module Inquirex
|
|
|
6
6
|
# What one dataset folder shares across its cases: the flow in dsl.rb,
|
|
7
7
|
# whose single `extract` step supplies the prompt and the schema, and
|
|
8
8
|
# schema.rb, that schema copied out verbatim so a change to either is
|
|
9
|
-
# caught by `
|
|
9
|
+
# caught by `evals verify` instead of silently moving the scores.
|
|
10
10
|
#
|
|
11
11
|
# evals/datasets/05-managed-it-services/
|
|
12
12
|
# dsl.rb the flow, loaded with unsafe: true (a file we author)
|
|
@@ -61,7 +61,7 @@ module Inquirex
|
|
|
61
61
|
# @return [Symbol]
|
|
62
62
|
def source_step = node.from_steps.first
|
|
63
63
|
|
|
64
|
-
# Offline checks of the shared files, for `
|
|
64
|
+
# Offline checks of the shared files, for `evals verify`.
|
|
65
65
|
#
|
|
66
66
|
# @return [Array<String>] problems; empty when the dataset is sound
|
|
67
67
|
def problems
|
|
@@ -4,7 +4,7 @@ module Inquirex
|
|
|
4
4
|
module LLM
|
|
5
5
|
module Evals
|
|
6
6
|
# The gem's {OpenAIAdapter} with its transport swapped for an
|
|
7
|
-
#
|
|
7
|
+
# llm-evals {LLM::Evals::Model}. Everything the model sees and
|
|
8
8
|
# everything done to its answer is production's: the system prompt,
|
|
9
9
|
# the schema contract, the user prompt built from the step's `from`
|
|
10
10
|
# answers, fence stripping, JSON parsing and the canonicalization of
|
|
@@ -18,13 +18,13 @@ module Inquirex
|
|
|
18
18
|
# OpenAIAdapter insists on a key; the eval model holds the real one.
|
|
19
19
|
NO_KEY = "(unused: the eval model carries its own key)"
|
|
20
20
|
|
|
21
|
-
# @return [
|
|
21
|
+
# @return [LLM::Evals::Completion, nil] what the model answered, once asked
|
|
22
22
|
attr_reader :completion
|
|
23
23
|
|
|
24
24
|
# @return [Hash{Symbol => String}, nil] the system and user prompts, once sent
|
|
25
25
|
attr_reader :prompt
|
|
26
26
|
|
|
27
|
-
# @param model [
|
|
27
|
+
# @param model [LLM::Evals::Model]
|
|
28
28
|
def initialize(model)
|
|
29
29
|
super(api_key: NO_KEY, model: model.id)
|
|
30
30
|
@model = model
|
|
@@ -13,36 +13,36 @@ module Inquirex
|
|
|
13
13
|
# share the model answered anyway; it is nil when the truth has no
|
|
14
14
|
# null, so a dataset of complete texts neither earns nor loses by it.
|
|
15
15
|
# `invalid_rate` is the share of fields the schema would reject.
|
|
16
|
-
class Scorer <
|
|
16
|
+
class Scorer < ::LLM::Evals::Scorer
|
|
17
17
|
METRICS = [
|
|
18
|
-
|
|
18
|
+
::LLM::Evals::Metric.new(name: "score",
|
|
19
19
|
primary: true,
|
|
20
20
|
description: "Mean field value: correct 1.0, missed 0.2, hallucinated 0.15, incorrect 0.1, invalid 0; " \
|
|
21
21
|
"a multi_enum is the mean of its elements."),
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
::LLM::Evals::Metric.new(name: "exact", description: "1 when every field is correct."),
|
|
23
|
+
::LLM::Evals::Metric.new(name: "hallucinated_rate",
|
|
24
24
|
description: "Of the fields the text never answers (truth null), the share the model answered anyway; " \
|
|
25
25
|
"nothing recorded when the truth has no null."),
|
|
26
|
-
|
|
26
|
+
::LLM::Evals::Metric.new(name: "invalid_rate",
|
|
27
27
|
description: "Share of fields whose value the schema does not allow: off the list, wrong type.")
|
|
28
28
|
].freeze
|
|
29
29
|
|
|
30
|
-
# How the gem's field types read to {
|
|
30
|
+
# How the gem's field types read to {LLM::Evals::SchemaValidator}.
|
|
31
31
|
VALIDATOR_TYPES = { enum: :enum, multi_enum: :multi_enum, integer: :integer, boolean: :boolean,
|
|
32
32
|
decimal: :number, currency: :number }.freeze
|
|
33
33
|
|
|
34
|
-
# @return [Array<
|
|
34
|
+
# @return [Array<LLM::Evals::Metric>]
|
|
35
35
|
def metrics = METRICS
|
|
36
36
|
|
|
37
|
-
# @param kase [
|
|
38
|
-
# @param reply [
|
|
37
|
+
# @param kase [LLM::Evals::Case]
|
|
38
|
+
# @param reply [LLM::Evals::Reply]
|
|
39
39
|
# @param context [Hash] `:schema`, the {Inquirex::LLM::Schema} the dataset extracts
|
|
40
|
-
# @return [
|
|
40
|
+
# @return [LLM::Evals::Score]
|
|
41
41
|
def call(kase, reply, context:)
|
|
42
42
|
schema = context.fetch(:schema)
|
|
43
43
|
fields = field_scores(schema, kase.expected, reply.raw)
|
|
44
44
|
open = fields.select { kase.expected[it.field].nil? }
|
|
45
|
-
|
|
45
|
+
::LLM::Evals::Score.new(
|
|
46
46
|
value: mean(fields.map(&:value)),
|
|
47
47
|
metrics: { "score" => mean(fields.map(&:value)),
|
|
48
48
|
"exact" => fields.all?(&:correct?) ? 1.0 : 0.0,
|
|
@@ -74,7 +74,7 @@ module Inquirex
|
|
|
74
74
|
# Checks expected.json against the schema, every field nullable: a
|
|
75
75
|
# null is how the truth says the text never answers it.
|
|
76
76
|
#
|
|
77
|
-
# @param kase [
|
|
77
|
+
# @param kase [LLM::Evals::Case]
|
|
78
78
|
# @param context [Hash]
|
|
79
79
|
# @return [Array<String>] unknown keys, missing keys, off-list values, wrong types
|
|
80
80
|
def verify(kase, context:)
|
|
@@ -82,10 +82,10 @@ module Inquirex
|
|
|
82
82
|
fields = schema.field_specs.to_h do |name, spec|
|
|
83
83
|
[name.to_s, { type: VALIDATOR_TYPES.fetch(spec.type, :string), enum: spec.values, nullable: true }]
|
|
84
84
|
end
|
|
85
|
-
|
|
85
|
+
::LLM::Evals::SchemaValidator.new(fields).problems(kase.expected)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
-
# @param kase [
|
|
88
|
+
# @param kase [LLM::Evals::Case]
|
|
89
89
|
# @return [String] "answered 7/10"
|
|
90
90
|
def summary(kase)
|
|
91
91
|
return nil unless kase.expected.is_a?(Hash)
|
data/lib/inquirex/llm/evals.rb
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# The extraction eval suite's Ruby: an
|
|
3
|
+
# The extraction eval suite's Ruby: an llm-evals Adapter that runs one
|
|
4
4
|
# case through the gem's production prompts, and a Scorer that grades the
|
|
5
5
|
# reply field by field. Loaded by `require "inquirex/llm/evals"` from
|
|
6
|
-
# evals/.
|
|
7
|
-
#
|
|
6
|
+
# evals/.llm-evals.config.yml, never by `require "inquirex-llm"`, because
|
|
7
|
+
# llm-evals is the suite's dependency and not the gem's.
|
|
8
8
|
begin
|
|
9
|
-
require "
|
|
9
|
+
require "llm/evals"
|
|
10
10
|
rescue LoadError => e
|
|
11
11
|
raise LoadError,
|
|
12
|
-
"inquirex/llm/evals needs the
|
|
12
|
+
"inquirex/llm/evals needs the llm-evals gem, which is not a dependency of inquirex-llm: " \
|
|
13
13
|
"add it to the :evals group of your Gemfile (#{e.message})"
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -22,7 +22,7 @@ require_relative "evals/scorer"
|
|
|
22
22
|
|
|
23
23
|
module Inquirex
|
|
24
24
|
module LLM
|
|
25
|
-
# The pieces
|
|
25
|
+
# The pieces llm-evals needs from this gem. {Adapter} owns everything
|
|
26
26
|
# the model sees and everything done to its answer; {Scorer} owns what a
|
|
27
27
|
# right answer is. Neither knows the other, and only the config file
|
|
28
28
|
# names them both.
|
data/lib/inquirex/llm/version.rb
CHANGED