legion-llm 0.3.12 → 0.3.13

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0551af82013a885240cd8d38ba1f991d470110d308925a8f4848b8650376d252
4
- data.tar.gz: fc2da425ddafa426f89375dbffd9afccc2c5d318207ed2bff0513dc57cf7dc07
3
+ metadata.gz: f6dc45bc6e985a3a6399ba3ed860bfb1ac9d3d9a0f31dda55a2f812d3c46e7cb
4
+ data.tar.gz: c3db21154b0b43de08e3e23b24416d9a7dc26a58eb10beb19835845b6ad83500
5
5
  SHA512:
6
- metadata.gz: 3ebfd45a16cd899050c44c0e53b0ae9952c8c87f46381b0af4356cda6d03ebff05084c3d66923ffd9f3012674b41005e080fa0350cdb4a2f799cbaa83e1cd4dc
7
- data.tar.gz: bfb977400e5c78caa90012af604ec47c7b4be94e1d268cfceb3b240b1e9731f678b4a0f6dc30de4df6e014e4714701b15f554e9a5858b3a94754aedaaa67da84
6
+ metadata.gz: 6bd0700aee69aab3d7dad4e3266855d6ddf28de1574a9b1e48e972b653f4af509720e53b2d8c34e84ac9599a325b539c5fc6c7ac765e6c62a846a40e2b6b9519
7
+ data.tar.gz: c2ffe0842728637165668508a68a690eb0a00596710108b4685f47e4fa8b78f24e634ec652e11d7f86ace856f0166299c6827e7bb7a4f1e9ed6e491ed97ca559
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Legion LLM Changelog
2
2
 
3
+ ## [0.3.13] - 2026-03-21
4
+
5
+ ### Added
6
+ - `Legion::LLM::Hooks::RagGuard` module with `check_rag_faithfulness` for post-generation RAG faithfulness evaluation via lex-eval
7
+ - `Legion::LLM::Hooks::ResponseGuard` module with `guard_response` as the central dispatch point for post-generation safety checks
8
+ - Response guard wired into `_dispatch_chat`: fires when `Legion::Settings[:llm][:response_guards][:enabled]` is true, attaches `_guard_result` metadata to the response hash without blocking
9
+ - RAG guard skips gracefully when lex-eval is unavailable (returns `reason: :eval_unavailable`) or context is not provided (returns `reason: :no_context`)
10
+ - Settings keys: `llm.rag_guard.enabled`, `llm.rag_guard.threshold` (default 0.7), `llm.rag_guard.evaluators` (default `[:faithfulness, :rag_relevancy]`)
11
+ - 19 new specs in `spec/legion/llm/hooks/rag_guard_spec.rb` and `spec/legion/llm/hooks/response_guard_spec.rb`
12
+
3
13
  ## [0.3.12] - 2026-03-19
4
14
 
5
15
  ### Added
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module LLM
5
+ module Hooks
6
+ module RagGuard
7
+ class << self
8
+ def check_rag_faithfulness(response:, context:, threshold: nil, evaluators: nil, **)
9
+ return { faithful: true, reason: :eval_unavailable } unless eval_available?
10
+
11
+ resolved_threshold = threshold || settings_threshold
12
+ resolved_evaluators = evaluators || settings_evaluators
13
+
14
+ scores = {}
15
+ flagged = []
16
+
17
+ resolved_evaluators.each do |evaluator_name|
18
+ score = run_evaluator(evaluator_name, response: response, context: context)
19
+ scores[evaluator_name] = score
20
+ flagged << evaluator_name if score < resolved_threshold
21
+ end
22
+
23
+ faithful = flagged.empty?
24
+ details = build_details(scores, resolved_threshold, faithful)
25
+
26
+ { faithful: faithful, scores: scores, flagged_evaluators: flagged, details: details }
27
+ rescue StandardError => e
28
+ Legion::Logging.warn "RagGuard evaluation error: #{e.message}" if logging_available?
29
+ { faithful: true, reason: :eval_error }
30
+ end
31
+
32
+ private
33
+
34
+ def eval_available?
35
+ defined?(Legion::Extensions::Eval::Client)
36
+ end
37
+
38
+ def logging_available?
39
+ Legion.const_defined?('Logging')
40
+ end
41
+
42
+ def settings_threshold
43
+ val = Legion::Settings.dig(:llm, :rag_guard, :threshold) if Legion.const_defined?('Settings')
44
+ val || 0.7
45
+ end
46
+
47
+ def settings_evaluators
48
+ val = Legion::Settings.dig(:llm, :rag_guard, :evaluators) if Legion.const_defined?('Settings')
49
+ val || %i[faithfulness rag_relevancy]
50
+ end
51
+
52
+ def run_evaluator(evaluator_name, response:, context:)
53
+ client = Legion::Extensions::Eval::Client.new
54
+ result = client.run_evaluation(
55
+ evaluator_name: evaluator_name,
56
+ inputs: [{ input: context.to_s, output: response.to_s, expected: nil }]
57
+ )
58
+ result.dig(:summary, :avg_score) || 0.0
59
+ rescue StandardError
60
+ 0.0
61
+ end
62
+
63
+ def build_details(scores, threshold, faithful)
64
+ score_parts = scores.map { |k, v| "#{k}=#{v.round(3)}" }.join(', ')
65
+ status = faithful ? 'passed' : 'failed'
66
+ "RAG faithfulness check #{status} (threshold=#{threshold}): #{score_parts}"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module LLM
5
+ module Hooks
6
+ module ResponseGuard
7
+ GUARD_REGISTRY = {
8
+ rag: RagGuard
9
+ }.freeze
10
+
11
+ class << self
12
+ def guard_response(response:, context: nil, guards: [:rag], **)
13
+ guard_results = {}
14
+
15
+ guards.each do |guard_name|
16
+ guard_mod = GUARD_REGISTRY[guard_name.to_sym]
17
+ next unless guard_mod
18
+
19
+ guard_results[guard_name] = dispatch_guard(guard_mod, guard_name,
20
+ response: response, context: context)
21
+ end
22
+
23
+ passed = guard_results.values.all? { |r| r[:faithful] != false }
24
+
25
+ { passed: passed, guards: guard_results }
26
+ rescue StandardError => e
27
+ Legion::Logging.warn "ResponseGuard error: #{e.message}" if Legion.const_defined?('Logging')
28
+ { passed: true, guards: {} }
29
+ end
30
+
31
+ private
32
+
33
+ def dispatch_guard(guard_mod, guard_name, response:, context:)
34
+ case guard_name.to_sym
35
+ when :rag
36
+ return { faithful: true, reason: :no_context } if context.nil?
37
+
38
+ guard_mod.check_rag_faithfulness(response: response, context: context)
39
+ else
40
+ guard_mod.check(response: response, context: context)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'legion/llm/hooks/rag_guard'
4
+ require 'legion/llm/hooks/response_guard'
5
+
3
6
  module Legion
4
7
  module LLM
5
8
  module Hooks
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module LLM
5
- VERSION = '0.3.12'
5
+ VERSION = '0.3.13'
6
6
  end
7
7
  end
data/lib/legion/llm.rb CHANGED
@@ -184,7 +184,7 @@ module Legion
184
184
 
185
185
  private
186
186
 
187
- def _dispatch_chat(model:, provider:, intent:, tier:, escalate:, max_escalations:, quality_check:, message:, **)
187
+ def _dispatch_chat(model:, provider:, intent:, tier:, escalate:, max_escalations:, quality_check:, message:, **kwargs)
188
188
  messages = message.is_a?(Array) ? message : [{ role: 'user', content: message.to_s }]
189
189
  resolved_model = model || settings[:default_model]
190
190
 
@@ -196,11 +196,11 @@ module Legion
196
196
  result = if gateway_loaded? && message
197
197
  gateway_chat(model: model, provider: provider, intent: intent,
198
198
  tier: tier, message: message, escalate: escalate,
199
- max_escalations: max_escalations, quality_check: quality_check, **)
199
+ max_escalations: max_escalations, quality_check: quality_check, **kwargs)
200
200
  else
201
201
  chat_direct(model: model, provider: provider, intent: intent, tier: tier,
202
202
  escalate: escalate, max_escalations: max_escalations,
203
- quality_check: quality_check, message: message, **)
203
+ quality_check: quality_check, message: message, **kwargs)
204
204
  end
205
205
 
206
206
  if defined?(Legion::LLM::Hooks)
@@ -208,6 +208,8 @@ module Legion
208
208
  return blocked[:response] if blocked
209
209
  end
210
210
 
211
+ result = apply_response_guards(result, kwargs) if response_guards_enabled? && result.is_a?(Hash)
212
+
211
213
  result
212
214
  end
213
215
 
@@ -370,6 +372,24 @@ module Legion
370
372
  nil
371
373
  end
372
374
 
375
+ def response_guards_enabled?
376
+ settings.dig(:response_guards, :enabled) == true
377
+ end
378
+
379
+ def apply_response_guards(result, kwargs)
380
+ context = kwargs[:context]
381
+ response_text = result[:response] || result[:content]
382
+ guard_result = Hooks::ResponseGuard.guard_response(
383
+ response: response_text, context: context
384
+ )
385
+
386
+ Legion::Logging.warn "Response guard failed: #{guard_result.inspect}" if !guard_result[:passed] && Legion.const_defined?('Logging')
387
+
388
+ result.merge(_guard_result: guard_result)
389
+ rescue StandardError
390
+ result
391
+ end
392
+
373
393
  def cacheable?(cache_opt, temperature, message)
374
394
  cache_opt != false && temperature.to_f.zero? && message && Cache.enabled?
375
395
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.12
4
+ version: 0.3.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -141,6 +141,8 @@ files:
141
141
  - lib/legion/llm/escalation_history.rb
142
142
  - lib/legion/llm/helpers/llm.rb
143
143
  - lib/legion/llm/hooks.rb
144
+ - lib/legion/llm/hooks/rag_guard.rb
145
+ - lib/legion/llm/hooks/response_guard.rb
144
146
  - lib/legion/llm/providers.rb
145
147
  - lib/legion/llm/quality_checker.rb
146
148
  - lib/legion/llm/response_cache.rb