rubric_llm 0.6.0 → 0.8.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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +29 -6
- data/lib/rubric_llm/config.rb +13 -2
- data/lib/rubric_llm/judge.rb +1 -0
- data/lib/rubric_llm/metrics/base.rb +23 -0
- data/lib/rubric_llm/metrics/context_precision.rb +11 -8
- data/lib/rubric_llm/metrics/context_recall.rb +15 -8
- data/lib/rubric_llm/metrics/correctness.rb +6 -2
- data/lib/rubric_llm/metrics/factual_accuracy.rb +22 -6
- data/lib/rubric_llm/metrics/faithfulness.rb +6 -3
- data/lib/rubric_llm/metrics/relevance.rb +5 -2
- data/lib/rubric_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: fe5f964e023687c815cf95b56673d05f3103f15a679ebb6f5d272d287c396163
|
|
4
|
+
data.tar.gz: f082d306d7e50347dc6a53caf453dc7e24abbd0213012711506692516ff38a47
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8cdba0f5196529eed16d2c19edaca418f8feedca62244724a206e6b4baa1ab9ef13660a3cf24f1711ad52e43856f325648b88ee8443da0cce7bee9a9f9b637d7
|
|
7
|
+
data.tar.gz: c2828756fca3eb3395567bd61ebac9b80bdffd12dbdd4b410f000b5001b68169be14101a95bde4c0541f98526c9bdc7ff665e22e2d918fda75072af4dd1e4dc2
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.8.0] - 2026-09-22
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Calculate faithfulness, context precision, and context recall scores from the judge's item-level decisions instead of its suggested score
|
|
13
|
+
- Define scoring criteria for correctness, relevance, and factual accuracy, with factual accuracy measuring contradictions against the reference rather than missing facts
|
|
14
|
+
- Reject missing or malformed metric details, including missing reasoning, empty claim or fact lists, invalid context indices, and factual accuracy scores that conflict with their discrepancy lists. Context indices must be 1-based integers. Evaluation returns `nil` with an error for these responses; direct metric calls raise `JudgeError`
|
|
15
|
+
|
|
16
|
+
## [0.7.0] - 2026-09-22
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Optional `thinking_effort` configuration and `RUBRIC_THINKING_EFFORT` environment variable for judge reasoning models, passed to RubyLLM's `with_thinking(effort: ...)`. The default leaves thinking controls unchanged; explicit `nil` ignores the environment variable
|
|
21
|
+
|
|
8
22
|
## [0.6.0] - 2026-09-18
|
|
9
23
|
|
|
10
24
|
### Changed
|
data/README.md
CHANGED
|
@@ -8,14 +8,14 @@ Lightweight LLM evaluation framework for Ruby, inspired by [DeepEval](https://gi
|
|
|
8
8
|
|
|
9
9
|
Provider-agnostic evaluation with pluggable metrics, statistical A/B comparison, and test framework integration: no Rails, no ActiveRecord, no UI. Works anywhere Ruby runs.
|
|
10
10
|
|
|
11
|
-
RubricLLM `0.
|
|
11
|
+
RubricLLM `0.7.0` supports RubyLLM `~> 2.0` and Ruby 3.4 or later. Applications that use RubyLLM 1.x should stay on RubricLLM `0.5.x`.
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
15
|
Update both gem constraints together:
|
|
16
16
|
|
|
17
17
|
```ruby
|
|
18
|
-
gem "rubric_llm", "~> 0.
|
|
18
|
+
gem "rubric_llm", "~> 0.7.0"
|
|
19
19
|
gem "ruby_llm", "~> 2.0"
|
|
20
20
|
```
|
|
21
21
|
|
|
@@ -30,7 +30,7 @@ If the application still has a RubyLLM 1.x constraint, update both constraints b
|
|
|
30
30
|
Or install directly:
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
gem install rubric_llm --version 0.
|
|
33
|
+
gem install rubric_llm --version 0.7.0
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
## Quick Start
|
|
@@ -92,6 +92,28 @@ end
|
|
|
92
92
|
|
|
93
93
|
Structured output support depends on the selected provider and model. RubricLLM sends its schema when RubyLLM reports structured output support. Otherwise it requests JSON text and validates the response object and score locally. Check the target provider's support before relying on a schema or a specific protocol. See RubyLLM's [2.0 upgrade guide](https://rubyllm.com/next/upgrading/), [request control guide](https://rubyllm.com/next/chat-request-control/), and [structured output support](https://rubyllm.com/next/structured-output/).
|
|
94
94
|
|
|
95
|
+
### Judge Thinking Effort
|
|
96
|
+
|
|
97
|
+
Set `thinking_effort` when using a reasoning model to control the trade-off between judge quality, latency, and cost:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
RubricLLM.configure do |c|
|
|
101
|
+
c.judge_model = "gpt-6-astra"
|
|
102
|
+
c.judge_provider = :openai
|
|
103
|
+
c.temperature = nil
|
|
104
|
+
c.thinking_effort = :high
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Or configure an individual evaluation:
|
|
108
|
+
config = RubricLLM::Config.new(judge_model: "gpt-6-astra", temperature: nil, thinking_effort: "high")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
RubricLLM passes the value to RubyLLM's [`with_thinking(effort: ...)`](https://rubyllm.com/thinking/). Strings and symbols are accepted. Supported values depend on the model and provider; RubricLLM does not translate values or silently ignore unsupported settings. Request errors are reported as judge errors.
|
|
112
|
+
|
|
113
|
+
The default is `nil`: RubricLLM does not call `with_thinking`, so existing RubyLLM and provider behavior is unchanged. This does **not** disable reasoning. An omitted setting reads `RUBRIC_THINKING_EFFORT`; an explicit `thinking_effort: nil` ignores that environment variable. Use `:none` only when the selected model supports it.
|
|
114
|
+
|
|
115
|
+
Higher effort does not guarantee better scores. Compare results on your evaluation dataset. For providers that count reasoning tokens toward the output limit, increase `max_tokens` if the default 4,096 tokens leaves too little room for the JSON response.
|
|
116
|
+
|
|
95
117
|
### Environment Variables
|
|
96
118
|
|
|
97
119
|
All config fields can be set via environment variables:
|
|
@@ -101,6 +123,7 @@ All config fields can be set via environment variables:
|
|
|
101
123
|
| `RUBRIC_JUDGE_MODEL` | `gpt-4o` | Judge LLM model name |
|
|
102
124
|
| `RUBRIC_JUDGE_PROVIDER` | `openai` | RubyLLM provider |
|
|
103
125
|
| `RUBRIC_TEMPERATURE` | `0.0` | Judge temperature |
|
|
126
|
+
| `RUBRIC_THINKING_EFFORT` | unset | Model-specific reasoning effort, such as `high` |
|
|
104
127
|
| `RUBRIC_MAX_TOKENS` | `4096` | Max response tokens |
|
|
105
128
|
| `RUBRIC_MAX_RETRIES` | `2` | Max retries on transient failures |
|
|
106
129
|
| `RUBRIC_RETRY_BASE_DELAY` | `1.0` | Base delay (seconds) for exponential backoff |
|
|
@@ -142,14 +165,14 @@ RubyLLM classifies OpenAI's HTTP 429 `insufficient_quota` response as a rate-lim
|
|
|
142
165
|
|
|
143
166
|
### LLM-as-Judge Metrics
|
|
144
167
|
|
|
145
|
-
These metrics use a judge LLM to evaluate quality. Each
|
|
168
|
+
These metrics use a judge LLM to evaluate quality. Each returns a 0.0–1.0 score. Faithfulness counts supported answer claims, context precision counts relevant non-empty chunks, and context recall counts covered reference facts. These three scores are calculated from the judge's item-level decisions, not its suggested score. An empty or malformed item list (including an answer with no factual claims) is an evaluation error, not a quality score. Correctness, relevance, and factual accuracy use judge scores with metric-specific scoring criteria. The judge still decides what counts as a claim, fact, or relevant chunk, so scores are not deterministic across models.
|
|
146
169
|
|
|
147
170
|
| Metric | Question it answers | Requires |
|
|
148
171
|
|--------|-------------------|----------|
|
|
149
172
|
| **Correctness** | Does the answer match the known correct answer? | `ground_truth` |
|
|
150
173
|
| **Relevance** | Does the answer address what was asked? | `question` |
|
|
151
174
|
| **Context Precision** | Are the retrieved context chunks actually relevant? | `question`, `context` |
|
|
152
|
-
| **Factual Accuracy** |
|
|
175
|
+
| **Factual Accuracy** | Does the candidate contradict the reference (not omit it)? | `ground_truth` |
|
|
153
176
|
| **Context Recall** | Do the contexts cover the information in the ground truth? | `context`, `ground_truth` |
|
|
154
177
|
| **Faithfulness** | Is every claim in the answer supported by the context? | `context` |
|
|
155
178
|
|
|
@@ -403,7 +426,7 @@ Deep dives live in the [project wiki](https://github.com/dpaluy/rubric_llm/wiki)
|
|
|
403
426
|
## Requirements
|
|
404
427
|
|
|
405
428
|
- Ruby >= 3.4
|
|
406
|
-
- [ruby_llm](https://github.com/crmne/ruby_llm) ~> 2.0 for RubricLLM 0.
|
|
429
|
+
- [ruby_llm](https://github.com/crmne/ruby_llm) ~> 2.0 for RubricLLM 0.7.0
|
|
407
430
|
- An API key for your chosen LLM provider (set via RubyLLM configuration)
|
|
408
431
|
|
|
409
432
|
## Contributing
|
data/lib/rubric_llm/config.rb
CHANGED
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
module RubricLLM
|
|
4
4
|
class Config
|
|
5
5
|
attr_accessor :judge_model, :judge_provider, :temperature, :max_tokens, :custom_prompt,
|
|
6
|
-
:max_retries, :retry_base_delay, :concurrency
|
|
6
|
+
:max_retries, :retry_base_delay, :concurrency, :thinking_effort
|
|
7
7
|
|
|
8
8
|
def initialize(judge_model: nil, judge_provider: nil, # rubocop:disable Metrics/ParameterLists
|
|
9
9
|
temperature: Float(ENV.fetch("RUBRIC_TEMPERATURE", "0.0")), max_tokens: nil,
|
|
10
|
-
custom_prompt: nil, max_retries: nil, retry_base_delay: nil, concurrency: nil, validate: false
|
|
10
|
+
custom_prompt: nil, max_retries: nil, retry_base_delay: nil, concurrency: nil, validate: false,
|
|
11
|
+
thinking_effort: ENV.fetch("RUBRIC_THINKING_EFFORT", nil))
|
|
11
12
|
@judge_model = judge_model || ENV.fetch("RUBRIC_JUDGE_MODEL", "gpt-4o")
|
|
12
13
|
@judge_provider = (judge_provider || ENV.fetch("RUBRIC_JUDGE_PROVIDER", "openai")).to_sym
|
|
13
14
|
@temperature = temperature
|
|
15
|
+
@thinking_effort = thinking_effort
|
|
14
16
|
@max_tokens = max_tokens || Integer(ENV.fetch("RUBRIC_MAX_TOKENS", "4096"))
|
|
15
17
|
@custom_prompt = custom_prompt
|
|
16
18
|
@max_retries = max_retries || Integer(ENV.fetch("RUBRIC_MAX_RETRIES", "2"))
|
|
@@ -27,6 +29,7 @@ module RubricLLM
|
|
|
27
29
|
validate_judge_model
|
|
28
30
|
validate_judge_provider
|
|
29
31
|
validate_temperature
|
|
32
|
+
validate_thinking_effort
|
|
30
33
|
validate_max_tokens
|
|
31
34
|
validate_max_retries
|
|
32
35
|
validate_retry_base_delay
|
|
@@ -39,6 +42,7 @@ module RubricLLM
|
|
|
39
42
|
judge_model:,
|
|
40
43
|
judge_provider:,
|
|
41
44
|
temperature:,
|
|
45
|
+
thinking_effort:,
|
|
42
46
|
max_tokens:,
|
|
43
47
|
custom_prompt:,
|
|
44
48
|
max_retries:,
|
|
@@ -67,6 +71,13 @@ module RubricLLM
|
|
|
67
71
|
raise ConfigurationError, "temperature must be nil or between 0.0 and 2.0"
|
|
68
72
|
end
|
|
69
73
|
|
|
74
|
+
def validate_thinking_effort
|
|
75
|
+
return if thinking_effort.nil?
|
|
76
|
+
return if (thinking_effort.is_a?(String) || thinking_effort.is_a?(Symbol)) && !thinking_effort.to_s.strip.empty?
|
|
77
|
+
|
|
78
|
+
raise ConfigurationError, "thinking_effort must be nil or a non-empty string or symbol"
|
|
79
|
+
end
|
|
80
|
+
|
|
70
81
|
def validate_max_tokens
|
|
71
82
|
return if max_tokens.is_a?(Integer) && max_tokens.positive?
|
|
72
83
|
|
data/lib/rubric_llm/judge.rb
CHANGED
|
@@ -52,6 +52,7 @@ module RubricLLM
|
|
|
52
52
|
chat = RubyLLM.chat(model: config.judge_model, provider: config.judge_provider)
|
|
53
53
|
chat.with_temperature(config.temperature)
|
|
54
54
|
chat.with_max_output_tokens(config.max_tokens)
|
|
55
|
+
chat.with_thinking(effort: config.thinking_effort) unless config.thinking_effort.nil?
|
|
55
56
|
apply_response_schema(chat)
|
|
56
57
|
|
|
57
58
|
full_system_prompt = build_system_prompt(system_prompt)
|
|
@@ -35,6 +35,29 @@ module RubricLLM
|
|
|
35
35
|
def judge_eval(system_prompt:, user_prompt:)
|
|
36
36
|
judge.call(system_prompt:, user_prompt:)
|
|
37
37
|
end
|
|
38
|
+
|
|
39
|
+
def checked_items(result, key, label:, value_key:, text_key:, count: nil)
|
|
40
|
+
items = result[key]
|
|
41
|
+
valid = items.is_a?(Array) && items.any? && (count.nil? || items.size == count)
|
|
42
|
+
valid &&= items.all? do |item|
|
|
43
|
+
item.is_a?(Hash) && item[text_key].is_a?(String) && !item[text_key].strip.empty? &&
|
|
44
|
+
[true, false].include?(item[value_key])
|
|
45
|
+
end
|
|
46
|
+
raise JudgeError, "Judge response has invalid #{label}" unless valid
|
|
47
|
+
|
|
48
|
+
items
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def fraction(items, key)
|
|
52
|
+
items.count { |item| item[key] }.to_f / items.size
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def reasoning_for(result)
|
|
56
|
+
reasoning = result["reasoning"]
|
|
57
|
+
raise JudgeError, "Judge response missing reasoning" unless reasoning.is_a?(String) && !reasoning.strip.empty?
|
|
58
|
+
|
|
59
|
+
reasoning
|
|
60
|
+
end
|
|
38
61
|
end
|
|
39
62
|
end
|
|
40
63
|
end
|
|
@@ -5,7 +5,8 @@ module RubricLLM
|
|
|
5
5
|
class ContextPrecision < Base
|
|
6
6
|
SYSTEM_PROMPT = <<~PROMPT
|
|
7
7
|
You are an evaluation judge. Assess whether the retrieved contexts are relevant to the question.
|
|
8
|
-
Context precision
|
|
8
|
+
Context precision is the fraction of retrieved chunks useful for answering the question.
|
|
9
|
+
Classify every numbered chunk once, in order. Use the displayed 1-based index.
|
|
9
10
|
|
|
10
11
|
Respond with JSON only:
|
|
11
12
|
{
|
|
@@ -29,18 +30,20 @@ module RubricLLM
|
|
|
29
30
|
PROMPT
|
|
30
31
|
|
|
31
32
|
result = judge_eval(system_prompt: SYSTEM_PROMPT, user_prompt:)
|
|
32
|
-
normalize(result)
|
|
33
|
+
normalize(result, context_chunks.size)
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
private
|
|
36
37
|
|
|
37
|
-
def normalize(result)
|
|
38
|
+
def normalize(result, count)
|
|
39
|
+
context_scores = checked_items(result, "context_scores", label: "context scores", count:, value_key: "relevant", text_key: "reason")
|
|
40
|
+
unless context_scores.each_with_index.all? { |item, index| item["index"].is_a?(Integer) && item["index"] == index + 1 }
|
|
41
|
+
raise JudgeError, "Judge response has invalid context indices"
|
|
42
|
+
end
|
|
43
|
+
|
|
38
44
|
{
|
|
39
|
-
score:
|
|
40
|
-
details: {
|
|
41
|
-
context_scores: result["context_scores"],
|
|
42
|
-
reasoning: result["reasoning"]
|
|
43
|
-
}
|
|
45
|
+
score: fraction(context_scores, "relevant"),
|
|
46
|
+
details: { context_scores:, reasoning: reasoning_for(result) }
|
|
44
47
|
}
|
|
45
48
|
end
|
|
46
49
|
end
|
|
@@ -5,7 +5,10 @@ module RubricLLM
|
|
|
5
5
|
class ContextRecall < Base
|
|
6
6
|
SYSTEM_PROMPT = <<~PROMPT
|
|
7
7
|
You are an evaluation judge. Assess whether the provided contexts cover the information in the ground truth.
|
|
8
|
-
Context recall
|
|
8
|
+
Context recall is the fraction of facts in the ground truth supported by the contexts.
|
|
9
|
+
List every distinct factual claim in the ground truth. Mark it covered only if a numbered
|
|
10
|
+
context supports it, and use that context's 1-based index as source_context.
|
|
11
|
+
Use null for source_context when a fact is not covered.
|
|
9
12
|
|
|
10
13
|
Respond with JSON only:
|
|
11
14
|
{
|
|
@@ -31,18 +34,22 @@ module RubricLLM
|
|
|
31
34
|
PROMPT
|
|
32
35
|
|
|
33
36
|
result = judge_eval(system_prompt: SYSTEM_PROMPT, user_prompt:)
|
|
34
|
-
normalize(result)
|
|
37
|
+
normalize(result, context_chunks.size)
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
private
|
|
38
41
|
|
|
39
|
-
def normalize(result)
|
|
42
|
+
def normalize(result, count)
|
|
43
|
+
covered_facts = checked_items(result, "covered_facts", label: "covered facts", value_key: "covered", text_key: "fact")
|
|
44
|
+
valid_sources = covered_facts.all? do |item|
|
|
45
|
+
source = item["source_context"]
|
|
46
|
+
item["covered"] ? source.is_a?(Integer) && (1..count).cover?(source) : source.nil?
|
|
47
|
+
end
|
|
48
|
+
raise JudgeError, "Judge response has invalid source contexts" unless valid_sources
|
|
49
|
+
|
|
40
50
|
{
|
|
41
|
-
score:
|
|
42
|
-
details: {
|
|
43
|
-
covered_facts: result["covered_facts"],
|
|
44
|
-
reasoning: result["reasoning"]
|
|
45
|
-
}
|
|
51
|
+
score: fraction(covered_facts, "covered"),
|
|
52
|
+
details: { covered_facts:, reasoning: reasoning_for(result) }
|
|
46
53
|
}
|
|
47
54
|
end
|
|
48
55
|
end
|
|
@@ -5,7 +5,11 @@ module RubricLLM
|
|
|
5
5
|
class Correctness < Base
|
|
6
6
|
SYSTEM_PROMPT = <<~PROMPT
|
|
7
7
|
You are an evaluation judge. Assess whether the answer matches the ground truth.
|
|
8
|
-
Consider semantic equivalence, not just exact string matching.
|
|
8
|
+
Consider semantic equivalence, not just exact string matching. Score the answer against
|
|
9
|
+
the question and reference: 1.0 if it fully answers with no material errors, 0.5 if it
|
|
10
|
+
is partly correct but misses required information or includes a material error, and 0.0
|
|
11
|
+
if it is wrong or does not answer. Use intermediate values for partial cases and explain
|
|
12
|
+
what is correct, missing, or wrong. Do not require irrelevant reference details.
|
|
9
13
|
|
|
10
14
|
Respond with JSON only:
|
|
11
15
|
{
|
|
@@ -36,7 +40,7 @@ module RubricLLM
|
|
|
36
40
|
def normalize(result)
|
|
37
41
|
{
|
|
38
42
|
score: Float(result["score"]),
|
|
39
|
-
details: { reasoning: result
|
|
43
|
+
details: { reasoning: reasoning_for(result) }
|
|
40
44
|
}
|
|
41
45
|
end
|
|
42
46
|
end
|
|
@@ -5,7 +5,12 @@ module RubricLLM
|
|
|
5
5
|
class FactualAccuracy < Base
|
|
6
6
|
SYSTEM_PROMPT = <<~PROMPT
|
|
7
7
|
You are an evaluation judge. Compare the factual claims in the candidate answer against the reference answer.
|
|
8
|
-
Identify
|
|
8
|
+
Identify contradictions in candidate factual claims against the reference. Do not penalize
|
|
9
|
+
missing reference facts, which correctness measures. Score 1.0 if there are no contradictions,
|
|
10
|
+
0.5 if minor factual contradictions affect part of the answer, and 0.0 if major contradictions
|
|
11
|
+
undermine the answer. Use intermediate values for partial cases. Explain the score and list
|
|
12
|
+
each contradiction with severity minor or major. If the reference does not establish whether
|
|
13
|
+
a claim is true, do not call it a contradiction.
|
|
9
14
|
|
|
10
15
|
Respond with JSON only:
|
|
11
16
|
{
|
|
@@ -33,14 +38,25 @@ module RubricLLM
|
|
|
33
38
|
private
|
|
34
39
|
|
|
35
40
|
def normalize(result)
|
|
41
|
+
discrepancies = result["discrepancies"]
|
|
42
|
+
unless discrepancies.is_a?(Array) && discrepancies.all? { |item| valid_discrepancy?(item) }
|
|
43
|
+
raise JudgeError, "Judge response has invalid discrepancies"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
score = Float(result["score"])
|
|
47
|
+
raise JudgeError, "Judge response score conflicts with no discrepancies" if discrepancies.empty? && score < 1.0
|
|
48
|
+
raise JudgeError, "Judge response score conflicts with discrepancies" if discrepancies.any? && score >= 1.0
|
|
49
|
+
|
|
36
50
|
{
|
|
37
|
-
score
|
|
38
|
-
details: {
|
|
39
|
-
discrepancies: result["discrepancies"],
|
|
40
|
-
reasoning: result["reasoning"]
|
|
41
|
-
}
|
|
51
|
+
score:,
|
|
52
|
+
details: { discrepancies:, reasoning: reasoning_for(result) }
|
|
42
53
|
}
|
|
43
54
|
end
|
|
55
|
+
|
|
56
|
+
def valid_discrepancy?(item)
|
|
57
|
+
item.is_a?(Hash) && %w[claim reference].all? { |key| item[key].is_a?(String) && !item[key].strip.empty? } &&
|
|
58
|
+
%w[minor major].include?(item["severity"])
|
|
59
|
+
end
|
|
44
60
|
end
|
|
45
61
|
end
|
|
46
62
|
end
|
|
@@ -6,6 +6,8 @@ module RubricLLM
|
|
|
6
6
|
SYSTEM_PROMPT = <<~PROMPT
|
|
7
7
|
You are an evaluation judge. Assess whether the answer is faithful to the provided context.
|
|
8
8
|
A faithful answer only contains information that is supported by the context.
|
|
9
|
+
List every factual claim in the answer. Mark a claim supported only if the context supports it.
|
|
10
|
+
The score is the fraction of supported claims. If there are no factual claims, do not invent claims.
|
|
9
11
|
|
|
10
12
|
Respond with JSON only:
|
|
11
13
|
{
|
|
@@ -36,11 +38,12 @@ module RubricLLM
|
|
|
36
38
|
private
|
|
37
39
|
|
|
38
40
|
def normalize(result)
|
|
41
|
+
claims = checked_items(result, "claims", label: "claims", value_key: "supported", text_key: "claim")
|
|
39
42
|
{
|
|
40
|
-
score:
|
|
43
|
+
score: fraction(claims, "supported"),
|
|
41
44
|
details: {
|
|
42
|
-
claims
|
|
43
|
-
reasoning: result
|
|
45
|
+
claims:,
|
|
46
|
+
reasoning: reasoning_for(result)
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
49
|
end
|
|
@@ -5,7 +5,10 @@ module RubricLLM
|
|
|
5
5
|
class Relevance < Base
|
|
6
6
|
SYSTEM_PROMPT = <<~PROMPT
|
|
7
7
|
You are an evaluation judge. Assess whether the answer is relevant to the question.
|
|
8
|
-
A relevant answer directly addresses what was asked.
|
|
8
|
+
A relevant answer directly addresses what was asked, regardless of its factual accuracy.
|
|
9
|
+
Score 1.0 if it directly addresses all parts of the question, 0.5 if it addresses only
|
|
10
|
+
part or is mostly tangential, and 0.0 if it does not address the question. Use intermediate
|
|
11
|
+
values for partial cases and explain the score. Do not score correctness here.
|
|
9
12
|
|
|
10
13
|
Respond with JSON only:
|
|
11
14
|
{
|
|
@@ -32,7 +35,7 @@ module RubricLLM
|
|
|
32
35
|
def normalize(result)
|
|
33
36
|
{
|
|
34
37
|
score: Float(result["score"]),
|
|
35
|
-
details: { reasoning: result
|
|
38
|
+
details: { reasoning: reasoning_for(result) }
|
|
36
39
|
}
|
|
37
40
|
end
|
|
38
41
|
end
|
data/lib/rubric_llm/version.rb
CHANGED