lex-agentic-inference 0.1.9 → 0.1.10
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 +7 -0
- data/lib/legion/extensions/agentic/inference/prediction/client.rb +4 -2
- data/lib/legion/extensions/agentic/inference/prediction/helpers/prediction_store.rb +13 -2
- data/lib/legion/extensions/agentic/inference/prediction/runners/prediction.rb +30 -21
- data/lib/legion/extensions/agentic/inference/version.rb +1 -1
- data/spec/legion/extensions/agentic/inference/prediction/helpers/prediction_store_identity_spec.rb +96 -0
- data/spec/legion/extensions/agentic/inference/prediction/runners/prediction_identity_spec.rb +73 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d8d433f84cb7914fe6bbed1b49f6c4e2376d0fc2616981415d8bdabcec2549a0
|
|
4
|
+
data.tar.gz: 7bfd8bbd8210dfbd3a249735d3f4985fa5af6a33b93f9ee5d45e204fd36ab9ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 441a6d38e9fa3bc1911ae8c80a312336ae1462a810b262d309fb4fa07a5c5e0c55c76e1de748300928ba1c62e90dbb0fbc048037b83e0c03e1a387500d246180
|
|
7
|
+
data.tar.gz: 573dd89cbee59b4ea980ce2acea0afd0cc1d306e244be959f38081e88cd51bf0514e478a4ab5820f1b5b2bc155d965a147857a9d9c014f0c2292492d3fd68cf7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.10] - 2026-07-16
|
|
4
|
+
### Added
|
|
5
|
+
- Per-identity prediction store and rolling accuracy: `PredictionStore` is now keyed by `identity:`, isolating each partner's predictions and outcomes
|
|
6
|
+
- `PredictionStore#erase_partner!(identity:)` — removes all predictions and outcomes for the given identity, raising `ArgumentError` on mismatch
|
|
7
|
+
- `Runners::Prediction#erase_partner!(identity:)` — delegates to the per-identity store, enabling selective data erasure
|
|
8
|
+
- All runner methods (`predict`, `resolve_prediction`, `pending_predictions`, `prediction_accuracy`, `expire_stale_predictions`, `get_prediction`) accept `identity:` kwarg to route to the correct store
|
|
9
|
+
|
|
3
10
|
## [0.1.9] - 2026-05-07
|
|
4
11
|
### Fixed
|
|
5
12
|
- Abductive reasoning decay now handles restored hypotheses with no `last_evaluated_at` timestamp instead of crashing the update cycle.
|
|
@@ -13,12 +13,14 @@ module Legion
|
|
|
13
13
|
include Runners::Prediction
|
|
14
14
|
|
|
15
15
|
def initialize(**)
|
|
16
|
-
@
|
|
16
|
+
@prediction_stores = {}
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
def prediction_store(identity: nil)
|
|
22
|
+
@prediction_stores[identity] ||= Helpers::PredictionStore.new(identity: identity)
|
|
23
|
+
end
|
|
22
24
|
end
|
|
23
25
|
end
|
|
24
26
|
end
|
|
@@ -9,9 +9,10 @@ module Legion
|
|
|
9
9
|
module Prediction
|
|
10
10
|
module Helpers
|
|
11
11
|
class PredictionStore
|
|
12
|
-
attr_reader :predictions, :outcomes
|
|
12
|
+
attr_reader :predictions, :outcomes, :identity
|
|
13
13
|
|
|
14
|
-
def initialize
|
|
14
|
+
def initialize(identity: nil)
|
|
15
|
+
@identity = identity
|
|
15
16
|
@predictions = {}
|
|
16
17
|
@outcomes = []
|
|
17
18
|
end
|
|
@@ -78,6 +79,16 @@ module Legion
|
|
|
78
79
|
}
|
|
79
80
|
end
|
|
80
81
|
end
|
|
82
|
+
|
|
83
|
+
def erase_partner!(identity:)
|
|
84
|
+
raise ArgumentError, "identity mismatch: expected #{@identity.inspect}, got #{identity.inspect}" if @identity != identity
|
|
85
|
+
|
|
86
|
+
predictions_erased = @predictions.size
|
|
87
|
+
outcomes_erased = @outcomes.size
|
|
88
|
+
@predictions.clear
|
|
89
|
+
@outcomes.clear
|
|
90
|
+
{ erased: true, identity: identity, predictions_erased: predictions_erased, outcomes_erased: outcomes_erased }
|
|
91
|
+
end
|
|
81
92
|
end
|
|
82
93
|
end
|
|
83
94
|
end
|
|
@@ -12,9 +12,10 @@ module Legion
|
|
|
12
12
|
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
13
13
|
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
14
14
|
|
|
15
|
-
def predict(mode:, context: {}, confidence: nil, description: nil, **)
|
|
15
|
+
def predict(mode:, context: {}, confidence: nil, description: nil, identity: nil, **)
|
|
16
16
|
return { error: :invalid_mode, valid_modes: Helpers::Modes::REASONING_MODES } unless Helpers::Modes.valid_mode?(mode)
|
|
17
17
|
|
|
18
|
+
store = prediction_store(identity: identity)
|
|
18
19
|
prediction = {
|
|
19
20
|
prediction_id: SecureRandom.uuid,
|
|
20
21
|
mode: mode,
|
|
@@ -26,10 +27,10 @@ module Legion
|
|
|
26
27
|
horizon: Helpers::Modes::PREDICTION_HORIZON
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
store.store(prediction)
|
|
30
31
|
|
|
31
32
|
actionable = prediction[:confidence] >= Helpers::Modes::PREDICTION_CONFIDENCE_MIN
|
|
32
|
-
rolling_accuracy =
|
|
33
|
+
rolling_accuracy = store.accuracy
|
|
33
34
|
log.debug "[prediction] new: mode=#{mode} confidence=#{prediction[:confidence].round(2)} " \
|
|
34
35
|
"actionable=#{actionable} accuracy=#{rolling_accuracy.round(2)} id=#{prediction[:prediction_id][0..7]}"
|
|
35
36
|
|
|
@@ -40,12 +41,13 @@ module Legion
|
|
|
40
41
|
actionable: actionable,
|
|
41
42
|
rolling_accuracy: rolling_accuracy,
|
|
42
43
|
error_rate: (1.0 - rolling_accuracy).round(4),
|
|
43
|
-
resolved:
|
|
44
|
+
resolved: store.recently_resolved
|
|
44
45
|
}
|
|
45
46
|
end
|
|
46
47
|
|
|
47
|
-
def resolve_prediction(prediction_id:, outcome:, actual: nil, **)
|
|
48
|
-
|
|
48
|
+
def resolve_prediction(prediction_id:, outcome:, actual: nil, identity: nil, **)
|
|
49
|
+
store = prediction_store(identity: identity)
|
|
50
|
+
pred = store.resolve(prediction_id, outcome: outcome, actual: actual)
|
|
49
51
|
if pred
|
|
50
52
|
log.info "[prediction] resolved #{prediction_id[0..7]} outcome=#{outcome}"
|
|
51
53
|
record_outcome_trace(pred, outcome)
|
|
@@ -56,47 +58,54 @@ module Legion
|
|
|
56
58
|
end
|
|
57
59
|
end
|
|
58
60
|
|
|
59
|
-
def pending_predictions(**)
|
|
60
|
-
|
|
61
|
+
def pending_predictions(identity: nil, **)
|
|
62
|
+
store = prediction_store(identity: identity)
|
|
63
|
+
preds = store.pending
|
|
61
64
|
log.debug "[prediction] pending count=#{preds.size}"
|
|
62
65
|
{ predictions: preds, count: preds.size }
|
|
63
66
|
end
|
|
64
67
|
|
|
65
|
-
def prediction_accuracy(window: 100, **)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
def prediction_accuracy(window: 100, identity: nil, **)
|
|
69
|
+
store = prediction_store(identity: identity)
|
|
70
|
+
acc = store.accuracy(window: window)
|
|
71
|
+
total = store.outcomes.size
|
|
68
72
|
log.debug "[prediction] accuracy=#{acc.round(2)} total_outcomes=#{total}"
|
|
69
73
|
{ accuracy: acc, total_outcomes: total }
|
|
70
74
|
end
|
|
71
75
|
|
|
72
|
-
def expire_stale_predictions(**)
|
|
76
|
+
def expire_stale_predictions(identity: nil, **)
|
|
77
|
+
store = prediction_store(identity: identity)
|
|
73
78
|
expired_count = 0
|
|
74
79
|
|
|
75
|
-
|
|
80
|
+
store.pending.each do |pred|
|
|
76
81
|
age = Time.now.utc - pred[:created_at]
|
|
77
82
|
next unless age > pred[:horizon]
|
|
78
83
|
|
|
79
|
-
|
|
84
|
+
store.resolve(pred[:prediction_id], outcome: :expired, actual: nil)
|
|
80
85
|
expired_count += 1
|
|
81
86
|
end
|
|
82
87
|
|
|
83
|
-
remaining =
|
|
88
|
+
remaining = store.pending.size
|
|
84
89
|
log.debug "[prediction] expire sweep: expired=#{expired_count} remaining=#{remaining}"
|
|
85
90
|
|
|
86
91
|
{ expired_count: expired_count, remaining_pending: remaining }
|
|
87
92
|
end
|
|
88
93
|
|
|
89
|
-
def get_prediction(prediction_id:, **)
|
|
90
|
-
|
|
94
|
+
def get_prediction(prediction_id:, identity: nil, **)
|
|
95
|
+
store = prediction_store(identity: identity)
|
|
96
|
+
pred = store.get(prediction_id)
|
|
91
97
|
pred ? { found: true, prediction: pred } : { found: false }
|
|
92
98
|
end
|
|
93
99
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
def erase_partner!(identity:, **)
|
|
101
|
+
store = prediction_store(identity: identity)
|
|
102
|
+
result = store.erase_partner!(identity: identity)
|
|
103
|
+
log.info "[prediction] erased partner identity=#{identity}"
|
|
104
|
+
result
|
|
98
105
|
end
|
|
99
106
|
|
|
107
|
+
private
|
|
108
|
+
|
|
100
109
|
def estimate_confidence(mode, context)
|
|
101
110
|
base = case mode
|
|
102
111
|
when :fault_localization then 0.7
|
data/spec/legion/extensions/agentic/inference/prediction/helpers/prediction_store_identity_spec.rb
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Agentic::Inference::Prediction::Helpers::PredictionStore do
|
|
6
|
+
let(:store_x) { described_class.new(identity: 'partner-X') }
|
|
7
|
+
let(:store_y) { described_class.new(identity: 'partner-Y') }
|
|
8
|
+
|
|
9
|
+
let(:basic_prediction) do
|
|
10
|
+
{
|
|
11
|
+
mode: :fault_localization,
|
|
12
|
+
confidence: 0.75,
|
|
13
|
+
description: 'disk latency spike incoming',
|
|
14
|
+
status: :pending
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe 'identity isolation — reads for X never see Y data' do
|
|
19
|
+
it 'stores predictions under the correct identity' do
|
|
20
|
+
id_x = store_x.store(basic_prediction.dup)
|
|
21
|
+
expect(store_x.get(id_x)).not_to be_nil
|
|
22
|
+
expect(store_y.get(id_x)).to be_nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'X accuracy is independent of Y outcomes' do
|
|
26
|
+
3.times do
|
|
27
|
+
id = store_x.store(basic_prediction.dup)
|
|
28
|
+
store_x.resolve(id, outcome: :correct)
|
|
29
|
+
end
|
|
30
|
+
3.times do
|
|
31
|
+
id = store_y.store(basic_prediction.dup)
|
|
32
|
+
store_y.resolve(id, outcome: :incorrect)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
expect(store_x.accuracy).to eq(1.0)
|
|
36
|
+
expect(store_y.accuracy).to eq(0.0)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'X pending predictions do not include Y predictions' do
|
|
40
|
+
store_x.store(basic_prediction.dup)
|
|
41
|
+
id_y = store_y.store(basic_prediction.dup)
|
|
42
|
+
store_y.resolve(id_y, outcome: :correct)
|
|
43
|
+
|
|
44
|
+
expect(store_x.pending.size).to eq(1)
|
|
45
|
+
expect(store_y.pending.size).to eq(0)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'X count is independent of Y count' do
|
|
49
|
+
3.times { store_x.store(basic_prediction.dup) }
|
|
50
|
+
5.times { store_y.store(basic_prediction.dup) }
|
|
51
|
+
|
|
52
|
+
expect(store_x.count).to eq(3)
|
|
53
|
+
expect(store_y.count).to eq(5)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe '#identity' do
|
|
58
|
+
it 'exposes the identity it was initialized with' do
|
|
59
|
+
expect(store_x.identity).to eq('partner-X')
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe '#erase_partner!' do
|
|
64
|
+
it 'removes all predictions for the identity' do
|
|
65
|
+
3.times { store_x.store(basic_prediction.dup) }
|
|
66
|
+
store_x.erase_partner!(identity: 'partner-X')
|
|
67
|
+
expect(store_x.count).to eq(0)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'removes all outcomes for the identity' do
|
|
71
|
+
id = store_x.store(basic_prediction.dup)
|
|
72
|
+
store_x.resolve(id, outcome: :correct)
|
|
73
|
+
store_x.erase_partner!(identity: 'partner-X')
|
|
74
|
+
expect(store_x.outcomes).to be_empty
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'does not touch a different identity store' do
|
|
78
|
+
id_x = store_x.store(basic_prediction.dup)
|
|
79
|
+
store_x.resolve(id_x, outcome: :correct)
|
|
80
|
+
|
|
81
|
+
3.times { store_y.store(basic_prediction.dup) }
|
|
82
|
+
store_x.erase_partner!(identity: 'partner-X')
|
|
83
|
+
|
|
84
|
+
expect(store_y.count).to eq(3)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'returns a confirmation hash' do
|
|
88
|
+
result = store_x.erase_partner!(identity: 'partner-X')
|
|
89
|
+
expect(result).to include(erased: true, identity: 'partner-X')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'raises ArgumentError when identity does not match own identity' do
|
|
93
|
+
expect { store_x.erase_partner!(identity: 'partner-Y') }.to raise_error(ArgumentError)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/agentic/inference/prediction/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Agentic::Inference::Prediction::Runners::Prediction do
|
|
6
|
+
let(:client) { Legion::Extensions::Agentic::Inference::Prediction::Client.new }
|
|
7
|
+
|
|
8
|
+
describe 'per-identity isolation' do
|
|
9
|
+
it 'predictions for identity X are not visible to identity Y' do
|
|
10
|
+
result_x = client.predict(mode: :fault_localization, identity: 'alice')
|
|
11
|
+
result_y = client.get_prediction(prediction_id: result_x[:prediction_id], identity: 'bob')
|
|
12
|
+
expect(result_y[:found]).to be false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'accuracy for X is independent of Y outcomes' do
|
|
16
|
+
3.times do
|
|
17
|
+
pred = client.predict(mode: :fault_localization, identity: 'alice')
|
|
18
|
+
client.resolve_prediction(prediction_id: pred[:prediction_id], outcome: :correct, identity: 'alice')
|
|
19
|
+
end
|
|
20
|
+
3.times do
|
|
21
|
+
pred = client.predict(mode: :fault_localization, identity: 'bob')
|
|
22
|
+
client.resolve_prediction(prediction_id: pred[:prediction_id], outcome: :incorrect, identity: 'bob')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
acc_alice = client.prediction_accuracy(identity: 'alice')
|
|
26
|
+
acc_bob = client.prediction_accuracy(identity: 'bob')
|
|
27
|
+
|
|
28
|
+
expect(acc_alice[:accuracy]).to eq(1.0)
|
|
29
|
+
expect(acc_bob[:accuracy]).to eq(0.0)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'pending count for X does not include Y predictions' do
|
|
33
|
+
client.predict(mode: :fault_localization, identity: 'alice')
|
|
34
|
+
pred_bob = client.predict(mode: :fault_localization, identity: 'bob')
|
|
35
|
+
client.resolve_prediction(prediction_id: pred_bob[:prediction_id], outcome: :correct, identity: 'bob')
|
|
36
|
+
|
|
37
|
+
alice_pending = client.pending_predictions(identity: 'alice')
|
|
38
|
+
bob_pending = client.pending_predictions(identity: 'bob')
|
|
39
|
+
|
|
40
|
+
expect(alice_pending[:count]).to eq(1)
|
|
41
|
+
expect(bob_pending[:count]).to eq(0)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '#erase_partner!' do
|
|
46
|
+
it 'removes all predictions and outcomes for the identity' do
|
|
47
|
+
3.times do
|
|
48
|
+
pred = client.predict(mode: :fault_localization, identity: 'alice')
|
|
49
|
+
client.resolve_prediction(prediction_id: pred[:prediction_id], outcome: :correct, identity: 'alice')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
result = client.erase_partner!(identity: 'alice')
|
|
53
|
+
expect(result[:erased]).to be true
|
|
54
|
+
expect(result[:identity]).to eq('alice')
|
|
55
|
+
|
|
56
|
+
expect(client.pending_predictions(identity: 'alice')[:count]).to eq(0)
|
|
57
|
+
expect(client.prediction_accuracy(identity: 'alice')[:total_outcomes]).to eq(0)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'does not affect a different identity after erase' do
|
|
61
|
+
3.times { client.predict(mode: :fault_localization, identity: 'bob') }
|
|
62
|
+
client.erase_partner!(identity: 'alice')
|
|
63
|
+
|
|
64
|
+
expect(client.pending_predictions(identity: 'bob')[:count]).to eq(3)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe '#client responds to erase_partner!' do
|
|
69
|
+
it 'responds to erase_partner!' do
|
|
70
|
+
expect(client).to respond_to(:erase_partner!)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-agentic-inference
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -483,7 +483,9 @@ files:
|
|
|
483
483
|
- spec/legion/extensions/agentic/inference/prediction/actors/expire_predictions_spec.rb
|
|
484
484
|
- spec/legion/extensions/agentic/inference/prediction/client_spec.rb
|
|
485
485
|
- spec/legion/extensions/agentic/inference/prediction/helpers/modes_spec.rb
|
|
486
|
+
- spec/legion/extensions/agentic/inference/prediction/helpers/prediction_store_identity_spec.rb
|
|
486
487
|
- spec/legion/extensions/agentic/inference/prediction/helpers/prediction_store_spec.rb
|
|
488
|
+
- spec/legion/extensions/agentic/inference/prediction/runners/prediction_identity_spec.rb
|
|
487
489
|
- spec/legion/extensions/agentic/inference/prediction/runners/prediction_spec.rb
|
|
488
490
|
- spec/legion/extensions/agentic/inference/predictive_coding/client_spec.rb
|
|
489
491
|
- spec/legion/extensions/agentic/inference/predictive_coding/helpers/generative_model_spec.rb
|