lex-mesh 0.4.7 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd84b8f23017907b298f8f567e7bfeeda0a0fdcc4a8105dcb8b0228e57042e4b
4
- data.tar.gz: f207ee8b98551427165a196528377b6b57270ef1afeda079941a13918c4c907f
3
+ metadata.gz: 1de324c2997f1d71286302c373a002c92bde5a389cfc9baf9bdce04d373f4fc5
4
+ data.tar.gz: 365de50aed497f545fb7deb881c9c3b87156acf6f8e2a5d443393004868ae278
5
5
  SHA512:
6
- metadata.gz: 8d374bd0d78453bc8a0b1f3cfcde49a781fd1d43baf948ad56bbe0319b537329a19e0f397e76efc9341822d9d5fa57d8f528a710c7669f46e9d3eb407bede0ee
7
- data.tar.gz: 4eeea99fe27e30c68febc9b5456ffaf2f1ed0637476362985ce5e6ec58526e9e9d967436b965135296d2997fea7b1547ea556742e81f078360cf3fa9daa651e4
6
+ metadata.gz: 506f44f20bec8e35b4b5fc3eb389796e4664511fa0feea50dd962ead40b10cb977a4c0ebc518d4ab20d5cdeaed2b1c2374e3a4ef5a9011121b4cb614b6540ce3
7
+ data.tar.gz: 188f168003f79c0ed067cc75f98888fa2ecb0dee5244ad79d76eeeb5f5f538ddbd414614be318137806ce69b2d87894b1ec0e479f46ab7d2238700f0286a7989
@@ -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 }.to_s,
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
@@ -251,14 +251,31 @@ module Legion
251
251
  payload = trace[:content_payload]
252
252
  return nil unless payload.is_a?(String)
253
253
 
254
- match = payload.match(/domain.*?(\w+).*?value.*?(\w+).*?source.*?(\w+)/)
255
- return nil unless match
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
256
262
 
257
- { domain: match[1], value: match[2], source: match[3], confidence: trace[:confidence] }
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]
266
+
267
+ data
258
268
  rescue StandardError => _e
259
269
  nil
260
270
  end
261
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
+
262
279
  def compute_compatibility(profile)
263
280
  return nil unless profile.is_a?(Hash) && profile[:personality]
264
281
  return nil unless personality_available?
@@ -353,7 +370,15 @@ module Legion
353
370
  unique_channels = signals.map { |s| s[:channel] }.uniq
354
371
  return nil unless unique_channels.size >= 3
355
372
 
356
- { domain: 'format', value: 'adaptive', source: 'observation', confidence: 0.55 }
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
357
382
  end
358
383
  end
359
384
  end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Mesh
6
- VERSION = '0.4.7'
6
+ VERSION = '0.4.8'
7
7
  end
8
8
  end
9
9
  end
@@ -104,6 +104,24 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::PreferenceProfile do
104
104
  expect(result[:stored]).to be false
105
105
  expect(result[:reason]).to eq(:memory_not_available)
106
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
107
125
  end
108
126
 
109
127
  describe '.clear_preferences' do
@@ -136,6 +154,38 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::PreferenceProfile do
136
154
  end
137
155
  end
138
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
+
139
189
  describe '.erase_partner!' do
140
190
  before do
141
191
  described_class.clear_observations
@@ -321,7 +371,7 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::PreferenceProfile do
321
371
  expect(tone[:source]).to eq('observation')
322
372
  end
323
373
 
324
- it 'derives :adaptive format for mixed channel usage after threshold' do
374
+ it 'derives :structured format for mixed channel usage after threshold' do
325
375
  channels = %i[cli api chat rest]
326
376
  20.times.with_index do |i, _|
327
377
  ch = channels[i % channels.length]
@@ -333,10 +383,27 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::PreferenceProfile do
333
383
  inferred = described_class.inferred_preferences('user-mix')
334
384
  format = inferred.find { |p| p[:domain] == 'format' }
335
385
  expect(format).not_to be_nil
336
- expect(format[:value]).to eq('adaptive')
386
+ expect(format[:value]).to eq('structured')
337
387
  expect(format[:source]).to eq('observation')
338
388
  end
339
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
+
340
407
  it 'keeps separate observation counts per owner_id' do
341
408
  3.times { described_class.update_from_observation(owner_id: 'user-a', signals: cli_signal) }
342
409
  7.times { described_class.update_from_observation(owner_id: 'user-b', signals: cli_signal) }
data/spec/spec_helper.rb CHANGED
@@ -35,8 +35,8 @@ module Legion
35
35
  end
36
36
  end
37
37
 
38
- Legion::Settings.loader.settings[:client] ||= {}
39
- Legion::Settings.loader.settings[:client][:name] = 'test-agent' # rubocop:disable Legion/Llm/SettingsAccessPath
38
+ Legion::Settings[:client] ||= {}
39
+ Legion::Settings[:client][:name] = 'test-agent'
40
40
 
41
41
  require 'legion/extensions/mesh'
42
42
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-mesh
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.7
4
+ version: 0.4.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity