Module: Insika::Evals::JudgePanel
- Defined in:
- lib/insika/evals/judge.rb
Overview
Builds the configured judge panel from settings["evals"].
It lives HERE and not in evals/run.rb because the CLI is no longer the only
caller: the refinement gate scores a candidate with the SAME judges the operator
configured, and is explicit that a second copy of the judge would be the
worst possible outcome — the gate would then be grading against a rubric nobody
tuned. One builder, two callers.
Class Method Summary collapse
-
.build(settings, overrides: {}, chat_factory: nil, llm: nil) ⇒ Object
settings: the
evalshash (judges/quorum/aggregate/min_agreement). -
.judge(settings, **kw) ⇒ Object
Sugar for the callers that only want the judge (the gate).
-
.pairwise(settings, overrides: {}, chat_factory: nil, llm: nil) ⇒ Object
The SAME configured models, asked a different question.
- .resolve_models(settings, overrides) ⇒ Object
-
.ruby_llm_ask(model, provider, llm: nil) ⇒ Object
The default way to reach a model: RubyLLM, temperature 0, required lazily so nothing here loads a provider gem until a judge is actually configured.
Class Method Details
.build(settings, overrides: {}, chat_factory: nil, llm: nil) ⇒ Object
settings: the evals hash (judges/quorum/aggregate/min_agreement).
overrides: the CLI's flags, which win over the stored config.
-> [Judge, [model names]] | nil when nobody is configured to ask. NIL AND NOT
a no-op judge: a rubric'd case with no judge reads as judge_pending, which is
visible, where a judge that always passes would be silent.
llm: the graph's own RubyLLM context; nil = the global
constant. Only the deployment root and the CLI build judges today, and both
are one graph per process — the seam keeps the default honest for the day
an embedded graph gates a candidate on its own credentials.
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/insika/evals/judge.rb', line 171 def build(settings, overrides: {}, chat_factory: nil, llm: nil) settings = Coercion.deep_stringify(settings || {}) overrides = overrides.transform_keys(&:to_s) models = resolve_models(settings, overrides) return nil if models.empty? factory = chat_factory || ->(model, provider) { ruby_llm_ask(model, provider, llm: llm) } judge = Judge.new( asks: models.map { |m| factory.call(m["model"], m["provider"]) }, quorum: overrides["quorum"] || settings["quorum"] || 1, aggregate: overrides["aggregate"] || settings["aggregate"] || "median", min_agreement: overrides["min_agreement"] || settings["min_agreement"] || 0.5 ) [judge, models.map { |m| m["model"] }] end |
.judge(settings, **kw) ⇒ Object
Sugar for the callers that only want the judge (the gate).
188 |
# File 'lib/insika/evals/judge.rb', line 188 def judge(settings, **kw) = build(settings, **kw)&.first |
.pairwise(settings, overrides: {}, chat_factory: nil, llm: nil) ⇒ Object
The SAME configured models, asked a different question. One builder because "who judges here" is one operator decision: a pairwise panel configured apart from the rubric panel would let a run be graded by judges nobody chose. -> [Pairwise, [model names]] | nil when nobody is configured.
194 195 196 197 198 199 200 201 202 |
# File 'lib/insika/evals/judge.rb', line 194 def pairwise(settings, overrides: {}, chat_factory: nil, llm: nil) settings = Coercion.deep_stringify(settings || {}) models = resolve_models(settings, overrides.transform_keys(&:to_s)) return nil if models.empty? factory = chat_factory || ->(model, provider) { ruby_llm_ask(model, provider, llm: llm) } [Pairwise.new(asks: models.map { |m| factory.call(m["model"], m["provider"]) }), models.map { |m| m["model"] }] end |
.resolve_models(settings, overrides) ⇒ Object
204 205 206 207 208 209 210 211 |
# File 'lib/insika/evals/judge.rb', line 204 def resolve_models(settings, overrides) models = if Coercion.present?(overrides["judge_model"]) [{ "model" => overrides["judge_model"], "provider" => overrides["judge_provider"] }] else Array(settings["judges"]) end models.map { |m| Coercion.deep_stringify(m) }.reject { |m| m["model"].to_s.strip.empty? } end |
.ruby_llm_ask(model, provider, llm: nil) ⇒ Object
The default way to reach a model: RubyLLM, temperature 0, required lazily so nothing here loads a provider gem until a judge is actually configured.
215 216 217 218 219 220 221 222 |
# File 'lib/insika/evals/judge.rb', line 215 def ruby_llm_ask(model, provider, llm: nil) require "ruby_llm" llm ||= RubyLLM lambda do |prompt| llm.chat(model: model, provider: provider, assume_model_exists: true) .with_temperature(0).ask(prompt).content end end |