lex-mesh 0.4.6 → 0.4.8
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/lib/legion/extensions/mesh/actors/gossip.rb +1 -1
- data/lib/legion/extensions/mesh/actors/heartbeat.rb +1 -1
- data/lib/legion/extensions/mesh/actors/pending_expiry.rb +1 -1
- data/lib/legion/extensions/mesh/actors/silence_watchdog.rb +1 -1
- data/lib/legion/extensions/mesh/helpers/preference_profile.rb +58 -6
- data/lib/legion/extensions/mesh/version.rb +1 -1
- data/spec/legion/extensions/mesh/helpers/preference_profile_spec.rb +158 -3
- data/spec/legion/extensions/mesh/runners/preferences_spec.rb +1 -1
- data/spec/spec_helper.rb +2 -2
- 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: 1de324c2997f1d71286302c373a002c92bde5a389cfc9baf9bdce04d373f4fc5
|
|
4
|
+
data.tar.gz: 365de50aed497f545fb7deb881c9c3b87156acf6f8e2a5d443393004868ae278
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 506f44f20bec8e35b4b5fc3eb389796e4664511fa0feea50dd962ead40b10cb977a4c0ebc518d4ab20d5cdeaed2b1c2374e3a4ef5a9011121b4cb614b6540ce3
|
|
7
|
+
data.tar.gz: 188f168003f79c0ed067cc75f98888fa2ecb0dee5244ad79d76eeeb5f5f538ddbd414614be318137806ce69b2d87894b1ec0e479f46ab7d2238700f0286a7989
|
|
@@ -6,7 +6,7 @@ module Legion
|
|
|
6
6
|
module Extensions
|
|
7
7
|
module Mesh
|
|
8
8
|
module Actor
|
|
9
|
-
class Gossip < Legion::Extensions::Actors::Every
|
|
9
|
+
class Gossip < Legion::Extensions::Actors::Every
|
|
10
10
|
def runner_class
|
|
11
11
|
Legion::Extensions::Mesh::Runners::Mesh
|
|
12
12
|
end
|
|
@@ -6,7 +6,7 @@ module Legion
|
|
|
6
6
|
module Extensions
|
|
7
7
|
module Mesh
|
|
8
8
|
module Actor
|
|
9
|
-
class Heartbeat < Legion::Extensions::Actors::Every
|
|
9
|
+
class Heartbeat < Legion::Extensions::Actors::Every
|
|
10
10
|
def runner_class
|
|
11
11
|
Legion::Extensions::Mesh::Runners::Mesh
|
|
12
12
|
end
|
|
@@ -6,7 +6,7 @@ module Legion
|
|
|
6
6
|
module Extensions
|
|
7
7
|
module Mesh
|
|
8
8
|
module Actor
|
|
9
|
-
class PendingExpiry < Legion::Extensions::Actors::Every
|
|
9
|
+
class PendingExpiry < Legion::Extensions::Actors::Every
|
|
10
10
|
def runner_class
|
|
11
11
|
Legion::Extensions::Mesh::Runners::Preferences
|
|
12
12
|
end
|
|
@@ -6,7 +6,7 @@ module Legion
|
|
|
6
6
|
module Extensions
|
|
7
7
|
module Mesh
|
|
8
8
|
module Actor
|
|
9
|
-
class SilenceWatchdog < Legion::Extensions::Actors::Every
|
|
9
|
+
class SilenceWatchdog < Legion::Extensions::Actors::Every
|
|
10
10
|
def runner_class
|
|
11
11
|
Legion::Extensions::Mesh::Runners::Mesh
|
|
12
12
|
end
|
|
@@ -90,7 +90,7 @@ module Legion
|
|
|
90
90
|
confidence = SOURCE_CONFIDENCE.fetch(source.to_s, 0.5)
|
|
91
91
|
memory_runner.store_trace(
|
|
92
92
|
type: :semantic,
|
|
93
|
-
content_payload: { domain: domain, value: value, source: source }
|
|
93
|
+
content_payload: pref_json_dump({ domain: domain, value: value, source: source }),
|
|
94
94
|
domain_tags: ['preference', "preference:#{domain}", "owner:#{owner_id}"],
|
|
95
95
|
origin: :direct_experience,
|
|
96
96
|
confidence: confidence
|
|
@@ -103,7 +103,34 @@ module Legion
|
|
|
103
103
|
def clear_preferences(owner_id:, source: nil)
|
|
104
104
|
return { cleared: false, reason: :memory_not_available } unless memory_available?
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
runner = memory_runner
|
|
107
|
+
result = runner.retrieve_by_domain(domain_tag: "owner:#{owner_id}")
|
|
108
|
+
traces = result[:traces] || []
|
|
109
|
+
traces.each { |t| runner.delete_trace(trace_id: t[:trace_id]) }
|
|
110
|
+
{ cleared: true, owner_id: owner_id, source: source, count: traces.size }
|
|
111
|
+
rescue StandardError => e
|
|
112
|
+
{ cleared: false, reason: :error, message: e.message }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def erase_partner!(identity:)
|
|
116
|
+
key = identity.to_s
|
|
117
|
+
@observation_counts&.delete(key) # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
118
|
+
@observation_signals&.delete(key) # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
119
|
+
@inferred_preferences&.delete(key) # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
120
|
+
clear_mesh_cache(agent_id: key)
|
|
121
|
+
|
|
122
|
+
traces_deleted = 0
|
|
123
|
+
if memory_available?
|
|
124
|
+
runner = memory_runner
|
|
125
|
+
result = runner.retrieve_by_domain(domain_tag: "owner:#{key}")
|
|
126
|
+
traces = result[:traces] || []
|
|
127
|
+
traces.each { |t| runner.delete_trace(trace_id: t[:trace_id]) }
|
|
128
|
+
traces_deleted = traces.size
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
{ erased: true, identity: key, traces_deleted: traces_deleted }
|
|
132
|
+
rescue StandardError => e
|
|
133
|
+
{ erased: false, identity: key, reason: :error, message: e.message }
|
|
107
134
|
end
|
|
108
135
|
|
|
109
136
|
def preference_instructions(profile:)
|
|
@@ -224,14 +251,31 @@ module Legion
|
|
|
224
251
|
payload = trace[:content_payload]
|
|
225
252
|
return nil unless payload.is_a?(String)
|
|
226
253
|
|
|
227
|
-
|
|
228
|
-
|
|
254
|
+
parsed = parse_preference_payload_json(payload)
|
|
255
|
+
parsed ||= parse_preference_payload_legacy(payload)
|
|
256
|
+
return nil unless parsed
|
|
257
|
+
|
|
258
|
+
{ domain: parsed[:domain], value: parsed[:value], source: parsed[:source], confidence: trace[:confidence] }
|
|
259
|
+
rescue StandardError => _e
|
|
260
|
+
nil
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def parse_preference_payload_json(payload)
|
|
264
|
+
data = pref_json_load(payload)
|
|
265
|
+
return nil unless data.is_a?(Hash) && data[:domain] && data[:value] && data[:source]
|
|
229
266
|
|
|
230
|
-
|
|
267
|
+
data
|
|
231
268
|
rescue StandardError => _e
|
|
232
269
|
nil
|
|
233
270
|
end
|
|
234
271
|
|
|
272
|
+
def parse_preference_payload_legacy(payload)
|
|
273
|
+
match = payload.match(/domain.*?(\w+).*?value.*?(\w+).*?source.*?(\w+)/)
|
|
274
|
+
return nil unless match
|
|
275
|
+
|
|
276
|
+
{ domain: match[1], value: match[2], source: match[3] }
|
|
277
|
+
end
|
|
278
|
+
|
|
235
279
|
def compute_compatibility(profile)
|
|
236
280
|
return nil unless profile.is_a?(Hash) && profile[:personality]
|
|
237
281
|
return nil unless personality_available?
|
|
@@ -326,7 +370,15 @@ module Legion
|
|
|
326
370
|
unique_channels = signals.map { |s| s[:channel] }.uniq
|
|
327
371
|
return nil unless unique_channels.size >= 3
|
|
328
372
|
|
|
329
|
-
{ domain: 'format', value: '
|
|
373
|
+
{ domain: 'format', value: 'structured', source: 'observation', confidence: 0.55 }
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def pref_json_dump(data)
|
|
377
|
+
Legion::JSON.dump(data) # rubocop:disable Legion/HelperMigration/DirectJson
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def pref_json_load(payload)
|
|
381
|
+
Legion::JSON.load(payload) # rubocop:disable Legion/HelperMigration/DirectJson
|
|
330
382
|
end
|
|
331
383
|
end
|
|
332
384
|
end
|
|
@@ -12,12 +12,16 @@ unless defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
|
|
|
12
12
|
module Runners
|
|
13
13
|
module Traces
|
|
14
14
|
def retrieve_by_domain(**)
|
|
15
|
-
[]
|
|
15
|
+
{ count: 0, traces: [] }
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def store_trace(**)
|
|
19
19
|
{ stored: true }
|
|
20
20
|
end
|
|
21
|
+
|
|
22
|
+
def delete_trace(trace_id:, **)
|
|
23
|
+
{ deleted: true, trace_id: trace_id }
|
|
24
|
+
end
|
|
21
25
|
end
|
|
22
26
|
end
|
|
23
27
|
end
|
|
@@ -100,6 +104,24 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::PreferenceProfile do
|
|
|
100
104
|
expect(result[:stored]).to be false
|
|
101
105
|
expect(result[:reason]).to eq(:memory_not_available)
|
|
102
106
|
end
|
|
107
|
+
|
|
108
|
+
it 'serializes payload as JSON string' do
|
|
109
|
+
captured_payload = nil
|
|
110
|
+
runner_double = double('memory_runner')
|
|
111
|
+
allow(profile_mod).to receive(:memory_available?).and_return(true)
|
|
112
|
+
allow(profile_mod).to receive(:memory_runner).and_return(runner_double)
|
|
113
|
+
allow(runner_double).to receive(:store_trace) do |**args|
|
|
114
|
+
captured_payload = args[:content_payload]
|
|
115
|
+
{ stored: true }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
profile_mod.store_preference(owner_id: 'u1', domain: 'verbosity', value: 'concise', source: 'explicit')
|
|
119
|
+
|
|
120
|
+
parsed = Legion::JSON.load(captured_payload)
|
|
121
|
+
expect(parsed[:domain]).to eq('verbosity')
|
|
122
|
+
expect(parsed[:value]).to eq('concise')
|
|
123
|
+
expect(parsed[:source]).to eq('explicit')
|
|
124
|
+
end
|
|
103
125
|
end
|
|
104
126
|
|
|
105
127
|
describe '.clear_preferences' do
|
|
@@ -107,6 +129,122 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::PreferenceProfile do
|
|
|
107
129
|
result = profile_mod.clear_preferences(owner_id: 'user1', source: 'explicit')
|
|
108
130
|
expect(result[:cleared]).to be true
|
|
109
131
|
end
|
|
132
|
+
|
|
133
|
+
it 'actually deletes traces via memory runner when memory is available' do
|
|
134
|
+
trace = { trace_id: 'abc-123', domain_tags: ['preference', 'owner:user1'], confidence: 0.9 }
|
|
135
|
+
runner_double = double('memory_runner')
|
|
136
|
+
allow(profile_mod).to receive(:memory_available?).and_return(true)
|
|
137
|
+
allow(profile_mod).to receive(:memory_runner).and_return(runner_double)
|
|
138
|
+
allow(runner_double).to receive(:retrieve_by_domain).and_return({ traces: [trace] })
|
|
139
|
+
allow(runner_double).to receive(:delete_trace).and_return({ deleted: true })
|
|
140
|
+
|
|
141
|
+
result = profile_mod.clear_preferences(owner_id: 'user1')
|
|
142
|
+
|
|
143
|
+
expect(runner_double).to have_received(:retrieve_by_domain).with(hash_including(domain_tag: 'owner:user1'))
|
|
144
|
+
expect(runner_double).to have_received(:delete_trace).with(hash_including(trace_id: 'abc-123'))
|
|
145
|
+
expect(result[:cleared]).to be true
|
|
146
|
+
expect(result[:count]).to eq(1)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'returns not_available when memory is unavailable' do
|
|
150
|
+
allow(profile_mod).to receive(:memory_available?).and_return(false)
|
|
151
|
+
result = profile_mod.clear_preferences(owner_id: 'user1')
|
|
152
|
+
expect(result[:cleared]).to be false
|
|
153
|
+
expect(result[:reason]).to eq(:memory_not_available)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
describe '.parse_preference_trace' do
|
|
158
|
+
it 'parses JSON-encoded payload and returns symbol-keyed hash' do
|
|
159
|
+
payload = Legion::JSON.dump({ domain: 'verbosity', value: 'concise', source: 'explicit' })
|
|
160
|
+
trace = { content_payload: payload, confidence: 0.9 }
|
|
161
|
+
result = profile_mod.parse_preference_trace(trace)
|
|
162
|
+
expect(result[:domain]).to eq('verbosity')
|
|
163
|
+
expect(result[:value]).to eq('concise')
|
|
164
|
+
expect(result[:source]).to eq('explicit')
|
|
165
|
+
expect(result[:confidence]).to eq(0.9)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it 'falls back to legacy regex for old .to_s hash format' do
|
|
169
|
+
legacy_payload = '{:domain=>"verbosity", :value=>"concise", :source=>"explicit"}'
|
|
170
|
+
trace = { content_payload: legacy_payload, confidence: 0.7 }
|
|
171
|
+
result = profile_mod.parse_preference_trace(trace)
|
|
172
|
+
expect(result[:domain]).to eq('verbosity')
|
|
173
|
+
expect(result[:value]).to eq('concise')
|
|
174
|
+
expect(result[:source]).to eq('explicit')
|
|
175
|
+
expect(result[:confidence]).to eq(0.7)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it 'returns nil for a non-string payload' do
|
|
179
|
+
trace = { content_payload: nil, confidence: 0.5 }
|
|
180
|
+
expect(profile_mod.parse_preference_trace(trace)).to be_nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it 'returns nil for a payload that cannot be parsed by either method' do
|
|
184
|
+
trace = { content_payload: 'garbage data without structure', confidence: 0.5 }
|
|
185
|
+
expect(profile_mod.parse_preference_trace(trace)).to be_nil
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
describe '.erase_partner!' do
|
|
190
|
+
before do
|
|
191
|
+
described_class.clear_observations
|
|
192
|
+
described_class.clear_mesh_cache
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it 'removes observation counts for the identity' do
|
|
196
|
+
5.times { described_class.update_from_observation(owner_id: 'partner-x', signals: { channel: :cli, direct_address: false }) }
|
|
197
|
+
described_class.erase_partner!(identity: 'partner-x')
|
|
198
|
+
expect(described_class.observation_counts['partner-x']).to be_nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it 'removes inferred preferences for the identity' do
|
|
202
|
+
20.times { described_class.update_from_observation(owner_id: 'partner-x', signals: { channel: :cli, direct_address: false }) }
|
|
203
|
+
described_class.erase_partner!(identity: 'partner-x')
|
|
204
|
+
expect(described_class.inferred_preferences('partner-x')).to be_empty
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it 'removes mesh cache entries for the identity' do
|
|
208
|
+
described_class.store_mesh_profile(
|
|
209
|
+
agent_id: 'partner-x',
|
|
210
|
+
profile: { verbosity: :concise },
|
|
211
|
+
source_agent_id: 'partner-x'
|
|
212
|
+
)
|
|
213
|
+
described_class.erase_partner!(identity: 'partner-x')
|
|
214
|
+
expect(described_class.cached_mesh_profile(agent_id: 'partner-x')).to be_nil
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'deletes memory traces tagged with the identity' do
|
|
218
|
+
trace = { trace_id: 'xyz-789', domain_tags: ['preference', 'owner:partner-x'], confidence: 0.8 }
|
|
219
|
+
runner_double = double('memory_runner')
|
|
220
|
+
allow(described_class).to receive(:memory_available?).and_return(true)
|
|
221
|
+
allow(described_class).to receive(:memory_runner).and_return(runner_double)
|
|
222
|
+
allow(runner_double).to receive(:retrieve_by_domain).and_return({ traces: [trace] })
|
|
223
|
+
allow(runner_double).to receive(:delete_trace).and_return({ deleted: true })
|
|
224
|
+
|
|
225
|
+
described_class.erase_partner!(identity: 'partner-x')
|
|
226
|
+
|
|
227
|
+
expect(runner_double).to have_received(:delete_trace).with(hash_including(trace_id: 'xyz-789'))
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
it 'leaves data for other identities untouched' do
|
|
231
|
+
3.times { described_class.update_from_observation(owner_id: 'partner-y', signals: { channel: :cli, direct_address: false }) }
|
|
232
|
+
described_class.store_mesh_profile(
|
|
233
|
+
agent_id: 'partner-y',
|
|
234
|
+
profile: { verbosity: :detailed },
|
|
235
|
+
source_agent_id: 'partner-y'
|
|
236
|
+
)
|
|
237
|
+
described_class.erase_partner!(identity: 'partner-x')
|
|
238
|
+
expect(described_class.observation_counts['partner-y']).to eq(3)
|
|
239
|
+
expect(described_class.cached_mesh_profile(agent_id: 'partner-y')).not_to be_nil
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
it 'returns a result hash with erased identity and counts' do
|
|
243
|
+
result = described_class.erase_partner!(identity: 'partner-x')
|
|
244
|
+
expect(result).to be_a(Hash)
|
|
245
|
+
expect(result[:erased]).to be true
|
|
246
|
+
expect(result[:identity]).to eq('partner-x')
|
|
247
|
+
end
|
|
110
248
|
end
|
|
111
249
|
|
|
112
250
|
describe '.store_mesh_profile' do
|
|
@@ -233,7 +371,7 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::PreferenceProfile do
|
|
|
233
371
|
expect(tone[:source]).to eq('observation')
|
|
234
372
|
end
|
|
235
373
|
|
|
236
|
-
it 'derives :
|
|
374
|
+
it 'derives :structured format for mixed channel usage after threshold' do
|
|
237
375
|
channels = %i[cli api chat rest]
|
|
238
376
|
20.times.with_index do |i, _|
|
|
239
377
|
ch = channels[i % channels.length]
|
|
@@ -245,10 +383,27 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::PreferenceProfile do
|
|
|
245
383
|
inferred = described_class.inferred_preferences('user-mix')
|
|
246
384
|
format = inferred.find { |p| p[:domain] == 'format' }
|
|
247
385
|
expect(format).not_to be_nil
|
|
248
|
-
expect(format[:value]).to eq('
|
|
386
|
+
expect(format[:value]).to eq('structured')
|
|
249
387
|
expect(format[:source]).to eq('observation')
|
|
250
388
|
end
|
|
251
389
|
|
|
390
|
+
it 'derive_format never returns a value outside VALID_VALUES[:format]' do
|
|
391
|
+
channels = %i[cli api chat rest grpc websocket]
|
|
392
|
+
20.times.with_index do |i, _|
|
|
393
|
+
ch = channels[i % channels.length]
|
|
394
|
+
described_class.update_from_observation(
|
|
395
|
+
owner_id: 'user-valid',
|
|
396
|
+
signals: { content_type: :text, channel: ch, direct_address: false }
|
|
397
|
+
)
|
|
398
|
+
end
|
|
399
|
+
inferred = described_class.inferred_preferences('user-valid')
|
|
400
|
+
format_pref = inferred.find { |p| p[:domain] == 'format' }
|
|
401
|
+
if format_pref
|
|
402
|
+
valid_format_values = described_class::VALID_VALUES['format'].map(&:to_s)
|
|
403
|
+
expect(valid_format_values).to include(format_pref[:value])
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
252
407
|
it 'keeps separate observation counts per owner_id' do
|
|
253
408
|
3.times { described_class.update_from_observation(owner_id: 'user-a', signals: cli_signal) }
|
|
254
409
|
7.times { described_class.update_from_observation(owner_id: 'user-b', signals: cli_signal) }
|
|
@@ -31,7 +31,7 @@ RSpec.describe Legion::Extensions::Mesh::Runners::Preferences do
|
|
|
31
31
|
end)
|
|
32
32
|
stub_const('Legion::Extensions::Mesh::Transport::Messages::PreferenceQuery',
|
|
33
33
|
Class.new do
|
|
34
|
-
def initialize(**
|
|
34
|
+
def initialize(**opts); end
|
|
35
35
|
|
|
36
36
|
def publish; end
|
|
37
37
|
end)
|
data/spec/spec_helper.rb
CHANGED
|
@@ -35,8 +35,8 @@ module Legion
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
Legion::Settings
|
|
39
|
-
Legion::Settings
|
|
38
|
+
Legion::Settings[:client] ||= {}
|
|
39
|
+
Legion::Settings[:client][:name] = 'test-agent'
|
|
40
40
|
|
|
41
41
|
require 'legion/extensions/mesh'
|
|
42
42
|
|