Class: Silas::Eval::Assertions::Context
- Inherits:
-
Object
- Object
- Silas::Eval::Assertions::Context
- Defined in:
- lib/silas/eval/assertions.rb
Overview
Assertions read ONLY the transcript rows; the block runs in this context. They collect failures rather than raising, so one eval reports all misses.
Constant Summary collapse
- EXECUTED =
%w[completed started in_doubt].freeze
- MONEY =
/(?<cur>[£$€])\s?(?<amt>\d{1,3}(?:,\d{3})*(?:\.\d+)?|\d+(?:\.\d+)?)/
Instance Attribute Summary collapse
-
#failures ⇒ Object
readonly
Returns the value of attribute failures.
-
#skips ⇒ Object
readonly
Returns the value of attribute skips.
Instance Method Summary collapse
- #assert_approved(tool: nil) ⇒ Object
- #assert_final_matches(matcher) ⇒ Object
-
#assert_no_hallucinated_price(allowed: [], scale: [ 1, 100, 0.01 ]) ⇒ Object
No-hallucinated-price guard: every money amount in the final answer must trace to a number the agent actually saw (tool results or the user input), allowing pence<->pounds scaling.
- #assert_no_tool_called(name = nil) ⇒ Object
- #assert_parked(tool: nil) ⇒ Object
-
#assert_rubric(criteria) ⇒ Object
LLM-graded — opt-in; SKIPS offline (never fails the gate unless SILAS_EVAL_STRICT=1).
- #assert_tool_arg(name, key, value = :__unset, &pred) ⇒ Object
- #assert_tool_called(name, times: nil) ⇒ Object
- #assert_turn_completed ⇒ Object
- #assert_turn_failed(reason: nil) ⇒ Object
-
#initialize(transcript) ⇒ Context
constructor
A new instance of Context.
Constructor Details
#initialize(transcript) ⇒ Context
Returns a new instance of Context.
12 13 14 15 16 |
# File 'lib/silas/eval/assertions.rb', line 12 def initialize(transcript) @t = transcript @failures = [] @skips = [] end |
Instance Attribute Details
#failures ⇒ Object (readonly)
Returns the value of attribute failures.
10 11 12 |
# File 'lib/silas/eval/assertions.rb', line 10 def failures @failures end |
#skips ⇒ Object (readonly)
Returns the value of attribute skips.
10 11 12 |
# File 'lib/silas/eval/assertions.rb', line 10 def skips @skips end |
Instance Method Details
#assert_approved(tool: nil) ⇒ Object
52 53 54 55 |
# File 'lib/silas/eval/assertions.rb', line 52 def assert_approved(tool: nil) inv = tool ? @t.invocations_for(tool).last : @t.invocations.last check(inv&.approval_state == "approved", "expected #{tool || 'an invocation'} approved, state=#{inv&.approval_state.inspect}") end |
#assert_final_matches(matcher) ⇒ Object
38 39 40 |
# File 'lib/silas/eval/assertions.rb', line 38 def assert_final_matches(matcher) check(matcher === @t.final_text, "final answer #{@t.final_text.inspect} does not match #{matcher.inspect}") end |
#assert_no_hallucinated_price(allowed: [], scale: [ 1, 100, 0.01 ]) ⇒ Object
No-hallucinated-price guard: every money amount in the final answer must trace to a number the agent actually saw (tool results or the user input), allowing pence<->pounds scaling.
45 46 47 48 49 50 |
# File 'lib/silas/eval/assertions.rb', line 45 def assert_no_hallucinated_price(allowed: [], scale: [ 1, 100, 0.01 ]) stated = @t.final_text.scan(MONEY).map { |_c, a| a.delete(",").to_f } grounded = grounded_numbers + Array(allowed).map(&:to_f) bad = stated.reject { |n| grounded.any? { |g| scale.any? { |s| (g * s - n).abs < 0.005 } } } check(bad.empty?, "final answer states ungrounded amount(s) #{bad.inspect}; grounded=#{grounded.sort.inspect}") end |
#assert_no_tool_called(name = nil) ⇒ Object
24 25 26 27 |
# File 'lib/silas/eval/assertions.rb', line 24 def assert_no_tool_called(name = nil) bad = name ? execed(name) : @t.invocations.select { |i| EXECUTED.include?(i.status) } check(bad.empty?, "expected no#{" #{name}" if name} tool execution, saw #{bad.map(&:tool_name)}") end |
#assert_parked(tool: nil) ⇒ Object
57 58 59 60 61 |
# File 'lib/silas/eval/assertions.rb', line 57 def assert_parked(tool: nil) parked = @t.parked? parked &&= @t.invocations_for(tool).any? { |i| i.approval_state == "required" || i.status == "in_doubt" } if tool check(parked, "expected turn parked#{" on #{tool}" if tool}, status=#{@t.status}") end |
#assert_rubric(criteria) ⇒ Object
LLM-graded — opt-in; SKIPS offline (never fails the gate unless SILAS_EVAL_STRICT=1).
73 74 75 76 77 78 79 80 81 82 |
# File 'lib/silas/eval/assertions.rb', line 73 def assert_rubric(criteria) unless Grader.available? @skips << "assert_rubric skipped (no grader / offline)" return check(false, "assert_rubric strict skip (SILAS_EVAL_STRICT)") if ENV["SILAS_EVAL_STRICT"] == "1" return end verdict = Grader.grade(Grader.prompt(@t, criteria)) check(verdict.start_with?("PASS"), "rubric FAIL: #{verdict}") end |
#assert_tool_arg(name, key, value = :__unset, &pred) ⇒ Object
29 30 31 32 33 34 35 36 |
# File 'lib/silas/eval/assertions.rb', line 29 def assert_tool_arg(name, key, value = :__unset, &pred) inv = execed(name).last || @t.invocations_for(name).last return check(false, "#{name} was never called") unless inv got = inv.arguments[key.to_s] ok = pred ? pred.call(got) : got == value check(ok, "#{name}.#{key} expected #{pred ? 'predicate' : value.inspect}, got #{got.inspect}") end |
#assert_tool_called(name, times: nil) ⇒ Object
18 19 20 21 22 |
# File 'lib/silas/eval/assertions.rb', line 18 def assert_tool_called(name, times: nil) n = execed(name).size check(times ? n == times : n.positive?, "expected #{name} called#{" #{times}x" if times}, saw #{n} (#{summary})") end |
#assert_turn_completed ⇒ Object
63 64 65 |
# File 'lib/silas/eval/assertions.rb', line 63 def assert_turn_completed check(@t.completed?, "turn not completed (#{@t.status}/#{@t.turn.failure_reason})") end |
#assert_turn_failed(reason: nil) ⇒ Object
67 68 69 70 |
# File 'lib/silas/eval/assertions.rb', line 67 def assert_turn_failed(reason: nil) ok = @t.status == "failed" && (reason.nil? || @t.turn.failure_reason == reason) check(ok, "expected failed#{"/#{reason}" if reason}, got #{@t.status}/#{@t.turn.failure_reason}") end |