lex-mesh 0.4.3 → 0.4.5
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0bd1055e60b5e6c6d2e63c08f7aab32c7f03b294b8251da6b583516060e3fdec
|
|
4
|
+
data.tar.gz: aaee0b1ecd57d2e8673706de83ac842476b5aba0570a38ab57bce3ca6c17cee7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7e350395459a7499684bdc844fbd81b4a628f58e2656c2d68031e4ac3688ad954be2486e83023dbe54d2da0eca88e748133b187d6c9d6a9efc431564a0d9e4c5
|
|
7
|
+
data.tar.gz: 1de8d318e631a5e31c2babb44a01424a0371332c05678dce832a55ac1e895963a6a571a3e7a78cf5b65d951c3f81f5b1ddd63fbf801edb078d46d13df5de6552
|
|
@@ -22,6 +22,8 @@ module Legion
|
|
|
22
22
|
SOURCE_CONFIDENCE = {
|
|
23
23
|
'explicit' => 1.0,
|
|
24
24
|
'preference_learning' => 0.75,
|
|
25
|
+
'llm_inference' => 0.65,
|
|
26
|
+
'observation' => 0.55,
|
|
25
27
|
'personality' => 0.4,
|
|
26
28
|
'defaults' => 0.0
|
|
27
29
|
}.freeze
|
|
@@ -246,6 +248,86 @@ module Legion
|
|
|
246
248
|
def personality_available?
|
|
247
249
|
defined?(Legion::Extensions::Agentic::Self::Personality::Runners::Personality)
|
|
248
250
|
end
|
|
251
|
+
|
|
252
|
+
OBSERVATION_THRESHOLD = 20
|
|
253
|
+
|
|
254
|
+
def update_from_observation(owner_id:, signals:)
|
|
255
|
+
@observation_counts ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
256
|
+
@observation_signals ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
257
|
+
|
|
258
|
+
key = owner_id.to_s
|
|
259
|
+
@observation_counts[key] = (@observation_counts[key] || 0) + 1 # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
260
|
+
@observation_signals[key] ||= [] # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
261
|
+
@observation_signals[key] << signals # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
262
|
+
|
|
263
|
+
derive_and_store_observations(owner_id: key) if @observation_counts[key] >= OBSERVATION_THRESHOLD # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
264
|
+
|
|
265
|
+
{ updated: true }
|
|
266
|
+
rescue StandardError => _e
|
|
267
|
+
{ updated: false }
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def observation_counts
|
|
271
|
+
@observation_counts ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def inferred_preferences(owner_id)
|
|
275
|
+
@inferred_preferences ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
276
|
+
@inferred_preferences[owner_id.to_s] || [] # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def clear_observations
|
|
280
|
+
@observation_counts = {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
281
|
+
@observation_signals = {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
282
|
+
@inferred_preferences = {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def derive_and_store_observations(owner_id:)
|
|
286
|
+
@observation_signals ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
287
|
+
signals = @observation_signals[owner_id] || [] # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
288
|
+
return if signals.empty?
|
|
289
|
+
|
|
290
|
+
inferred = []
|
|
291
|
+
inferred << derive_verbosity(signals)
|
|
292
|
+
inferred << derive_tone(signals)
|
|
293
|
+
inferred << derive_format(signals)
|
|
294
|
+
inferred = inferred.compact
|
|
295
|
+
|
|
296
|
+
@inferred_preferences ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
297
|
+
@inferred_preferences[owner_id] = inferred # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
298
|
+
|
|
299
|
+
inferred.each do |pref|
|
|
300
|
+
store_preference(
|
|
301
|
+
owner_id: owner_id,
|
|
302
|
+
domain: pref[:domain],
|
|
303
|
+
value: pref[:value],
|
|
304
|
+
source: pref[:source]
|
|
305
|
+
)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def derive_verbosity(signals)
|
|
310
|
+
cli_count = signals.count { |s| s[:channel] == :cli }
|
|
311
|
+
cli_ratio = cli_count.to_f / signals.size
|
|
312
|
+
return nil unless cli_ratio >= 0.5
|
|
313
|
+
|
|
314
|
+
{ domain: 'verbosity', value: 'concise', source: 'observation', confidence: 0.65 }
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def derive_tone(signals)
|
|
318
|
+
da_count = signals.count { |s| s[:direct_address] }
|
|
319
|
+
da_ratio = da_count.to_f / signals.size
|
|
320
|
+
return nil unless da_ratio >= 0.5
|
|
321
|
+
|
|
322
|
+
{ domain: 'tone', value: 'conversational', source: 'observation', confidence: 0.65 }
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def derive_format(signals)
|
|
326
|
+
unique_channels = signals.map { |s| s[:channel] }.uniq
|
|
327
|
+
return nil unless unique_channels.size >= 3
|
|
328
|
+
|
|
329
|
+
{ domain: 'format', value: 'adaptive', source: 'observation', confidence: 0.55 }
|
|
330
|
+
end
|
|
249
331
|
end
|
|
250
332
|
end
|
|
251
333
|
end
|
|
@@ -191,6 +191,81 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::PreferenceProfile do
|
|
|
191
191
|
end
|
|
192
192
|
end
|
|
193
193
|
|
|
194
|
+
describe '.update_from_observation' do
|
|
195
|
+
before { described_class.clear_observations }
|
|
196
|
+
|
|
197
|
+
let(:cli_signal) { { content_type: :text, channel: :cli, direct_address: false } }
|
|
198
|
+
let(:chat_signal) { { content_type: :text, channel: :chat, direct_address: true } }
|
|
199
|
+
let(:mixed_signal) { { content_type: :text, channel: :api, direct_address: false } }
|
|
200
|
+
|
|
201
|
+
it 'returns { updated: true } on each call' do
|
|
202
|
+
result = described_class.update_from_observation(owner_id: 'user-obs', signals: cli_signal)
|
|
203
|
+
expect(result).to eq({ updated: true })
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it 'accumulates signals per owner_id' do
|
|
207
|
+
5.times { described_class.update_from_observation(owner_id: 'user-obs', signals: cli_signal) }
|
|
208
|
+
counts = described_class.observation_counts
|
|
209
|
+
expect(counts['user-obs']).to eq(5)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it 'does not infer preferences before threshold (20)' do
|
|
213
|
+
19.times { described_class.update_from_observation(owner_id: 'user-obs', signals: cli_signal) }
|
|
214
|
+
inferred = described_class.inferred_preferences('user-obs')
|
|
215
|
+
expect(inferred).to be_empty
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it 'derives :concise verbosity for CLI-heavy usage after threshold' do
|
|
219
|
+
20.times { described_class.update_from_observation(owner_id: 'user-cli', signals: cli_signal) }
|
|
220
|
+
inferred = described_class.inferred_preferences('user-cli')
|
|
221
|
+
verbosity = inferred.find { |p| p[:domain] == 'verbosity' }
|
|
222
|
+
expect(verbosity).not_to be_nil
|
|
223
|
+
expect(verbosity[:value]).to eq('concise')
|
|
224
|
+
expect(verbosity[:source]).to eq('observation')
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it 'derives :conversational tone for high direct_address ratio after threshold' do
|
|
228
|
+
20.times { described_class.update_from_observation(owner_id: 'user-da', signals: chat_signal) }
|
|
229
|
+
inferred = described_class.inferred_preferences('user-da')
|
|
230
|
+
tone = inferred.find { |p| p[:domain] == 'tone' }
|
|
231
|
+
expect(tone).not_to be_nil
|
|
232
|
+
expect(tone[:value]).to eq('conversational')
|
|
233
|
+
expect(tone[:source]).to eq('observation')
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it 'derives :adaptive format for mixed channel usage after threshold' do
|
|
237
|
+
channels = %i[cli api chat rest]
|
|
238
|
+
20.times.with_index do |i, _|
|
|
239
|
+
ch = channels[i % channels.length]
|
|
240
|
+
described_class.update_from_observation(
|
|
241
|
+
owner_id: 'user-mix',
|
|
242
|
+
signals: { content_type: :text, channel: ch, direct_address: false }
|
|
243
|
+
)
|
|
244
|
+
end
|
|
245
|
+
inferred = described_class.inferred_preferences('user-mix')
|
|
246
|
+
format = inferred.find { |p| p[:domain] == 'format' }
|
|
247
|
+
expect(format).not_to be_nil
|
|
248
|
+
expect(format[:value]).to eq('adaptive')
|
|
249
|
+
expect(format[:source]).to eq('observation')
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
it 'keeps separate observation counts per owner_id' do
|
|
253
|
+
3.times { described_class.update_from_observation(owner_id: 'user-a', signals: cli_signal) }
|
|
254
|
+
7.times { described_class.update_from_observation(owner_id: 'user-b', signals: cli_signal) }
|
|
255
|
+
counts = described_class.observation_counts
|
|
256
|
+
expect(counts['user-a']).to eq(3)
|
|
257
|
+
expect(counts['user-b']).to eq(7)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
it 'calls store_preference for each inferred preference after threshold' do
|
|
261
|
+
allow(described_class).to receive(:store_preference).and_call_original
|
|
262
|
+
20.times { described_class.update_from_observation(owner_id: 'user-store', signals: cli_signal) }
|
|
263
|
+
expect(described_class).to have_received(:store_preference).with(
|
|
264
|
+
hash_including(owner_id: 'user-store', source: 'observation')
|
|
265
|
+
).at_least(:once)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
194
269
|
describe '.preference_instructions' do
|
|
195
270
|
it 'generates natural language prompt instructions from profile' do
|
|
196
271
|
profile = {
|
|
@@ -211,4 +286,25 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::PreferenceProfile do
|
|
|
211
286
|
expect(instructions).to be_nil
|
|
212
287
|
end
|
|
213
288
|
end
|
|
289
|
+
|
|
290
|
+
describe 'SOURCE_CONFIDENCE' do
|
|
291
|
+
it 'includes observation source' do
|
|
292
|
+
expect(described_class::SOURCE_CONFIDENCE).to have_key('observation')
|
|
293
|
+
expect(described_class::SOURCE_CONFIDENCE['observation']).to eq(0.55)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
it 'includes llm_inference source' do
|
|
297
|
+
expect(described_class::SOURCE_CONFIDENCE).to have_key('llm_inference')
|
|
298
|
+
expect(described_class::SOURCE_CONFIDENCE['llm_inference']).to eq(0.65)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
it 'maintains descending confidence order for all sources' do
|
|
302
|
+
conf = described_class::SOURCE_CONFIDENCE
|
|
303
|
+
expect(conf['explicit']).to be > conf['preference_learning']
|
|
304
|
+
expect(conf['preference_learning']).to be > conf['llm_inference']
|
|
305
|
+
expect(conf['llm_inference']).to be > conf['observation']
|
|
306
|
+
expect(conf['observation']).to be > conf['personality']
|
|
307
|
+
expect(conf['personality']).to be > conf['defaults']
|
|
308
|
+
end
|
|
309
|
+
end
|
|
214
310
|
end
|