lex-mesh 0.4.3 → 0.4.4
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: 983bda71103db47ebf1fd1a9087afa36ac9073b108aed204f579204718f84672
|
|
4
|
+
data.tar.gz: 5a2a670f327317978f84f10684ba0b6f09031065e4402bece53309ccab96462e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 98a5c88d848ad732100824f8c6bcf4657413bb2fae5137a0a8d46cde3df54888596ddab738e4716a291e28cdfc6c81758d0cfdef73feaef70046e933983dd3e4
|
|
7
|
+
data.tar.gz: c3e8d3eec443be56de9b29817aff18ce83a7b98a5d84ceac5bf702206c33dd987990e77c31cf3c08359825b3916f44535747ad8419259201d6fcbbbff9c0309a
|
|
@@ -246,6 +246,86 @@ module Legion
|
|
|
246
246
|
def personality_available?
|
|
247
247
|
defined?(Legion::Extensions::Agentic::Self::Personality::Runners::Personality)
|
|
248
248
|
end
|
|
249
|
+
|
|
250
|
+
OBSERVATION_THRESHOLD = 20
|
|
251
|
+
|
|
252
|
+
def update_from_observation(owner_id:, signals:)
|
|
253
|
+
@observation_counts ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
254
|
+
@observation_signals ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
255
|
+
|
|
256
|
+
key = owner_id.to_s
|
|
257
|
+
@observation_counts[key] = (@observation_counts[key] || 0) + 1 # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
258
|
+
@observation_signals[key] ||= [] # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
259
|
+
@observation_signals[key] << signals # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
260
|
+
|
|
261
|
+
derive_and_store_observations(owner_id: key) if @observation_counts[key] >= OBSERVATION_THRESHOLD # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
262
|
+
|
|
263
|
+
{ updated: true }
|
|
264
|
+
rescue StandardError => _e
|
|
265
|
+
{ updated: false }
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def observation_counts
|
|
269
|
+
@observation_counts ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def inferred_preferences(owner_id)
|
|
273
|
+
@inferred_preferences ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
274
|
+
@inferred_preferences[owner_id.to_s] || [] # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def clear_observations
|
|
278
|
+
@observation_counts = {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
279
|
+
@observation_signals = {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
280
|
+
@inferred_preferences = {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def derive_and_store_observations(owner_id:)
|
|
284
|
+
@observation_signals ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
285
|
+
signals = @observation_signals[owner_id] || [] # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
286
|
+
return if signals.empty?
|
|
287
|
+
|
|
288
|
+
inferred = []
|
|
289
|
+
inferred << derive_verbosity(signals)
|
|
290
|
+
inferred << derive_tone(signals)
|
|
291
|
+
inferred << derive_format(signals)
|
|
292
|
+
inferred = inferred.compact
|
|
293
|
+
|
|
294
|
+
@inferred_preferences ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
295
|
+
@inferred_preferences[owner_id] = inferred # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
296
|
+
|
|
297
|
+
inferred.each do |pref|
|
|
298
|
+
store_preference(
|
|
299
|
+
owner_id: owner_id,
|
|
300
|
+
domain: pref[:domain],
|
|
301
|
+
value: pref[:value],
|
|
302
|
+
source: pref[:source]
|
|
303
|
+
)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def derive_verbosity(signals)
|
|
308
|
+
cli_count = signals.count { |s| s[:channel] == :cli }
|
|
309
|
+
cli_ratio = cli_count.to_f / signals.size
|
|
310
|
+
return nil unless cli_ratio >= 0.5
|
|
311
|
+
|
|
312
|
+
{ domain: 'verbosity', value: 'concise', source: 'observation', confidence: 0.65 }
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def derive_tone(signals)
|
|
316
|
+
da_count = signals.count { |s| s[:direct_address] }
|
|
317
|
+
da_ratio = da_count.to_f / signals.size
|
|
318
|
+
return nil unless da_ratio >= 0.5
|
|
319
|
+
|
|
320
|
+
{ domain: 'tone', value: 'conversational', source: 'observation', confidence: 0.65 }
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def derive_format(signals)
|
|
324
|
+
unique_channels = signals.map { |s| s[:channel] }.uniq
|
|
325
|
+
return nil unless unique_channels.size >= 3
|
|
326
|
+
|
|
327
|
+
{ domain: 'format', value: 'adaptive', source: 'observation', confidence: 0.55 }
|
|
328
|
+
end
|
|
249
329
|
end
|
|
250
330
|
end
|
|
251
331
|
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 = {
|