Module: Legion::Extensions::Mesh::Helpers::PreferenceProfile

Defined in:
lib/legion/extensions/mesh/helpers/preference_profile.rb

Constant Summary collapse

DEFAULTS =
{
  verbosity:       :normal,
  tone:            :professional,
  format:          :structured,
  technical_depth: :moderate
}.freeze
VALID_VALUES =
{
  'verbosity'       => %i[terse concise normal detailed verbose],
  'tone'            => %i[casual conversational professional formal],
  'format'          => %i[plain structured markdown],
  'technical_depth' => %i[high_level moderate deep implementation]
}.freeze
SOURCE_CONFIDENCE =
{
  'explicit'            => 1.0,
  'preference_learning' => 0.75,
  'llm_inference'       => 0.65,
  'observation'         => 0.55,
  'personality'         => 0.4,
  'defaults'            => 0.0
}.freeze
INSTRUCTION_MAP =
{
  verbosity:       {
    terse:    'Keep responses extremely short — one or two sentences max.',
    concise:  'Keep responses brief and to the point.',
    detailed: 'Provide thorough, detailed responses.',
    verbose:  'Be comprehensive and expansive in your responses.'
  },
  tone:            {
    casual:         'Use casual, friendly language.',
    conversational: 'Use a warm, conversational tone.',
    formal:         'Use formal, professional language.'
  },
  format:          {
    plain:    'Use plain text without formatting.',
    markdown: 'Use rich markdown formatting with headers and code blocks.'
  },
  technical_depth: {
    high_level:     'Keep explanations high-level, avoid implementation details.',
    deep:           'Include technical depth and implementation details.',
    implementation: 'Include implementation details, code examples, and edge cases.'
  }
}.freeze
MESH_CACHE_TTL =

1 hour default

3600
OBSERVATION_THRESHOLD =
20

Class Method Summary collapse

Class Method Details

.cached_mesh_profile(agent_id:, ttl: MESH_CACHE_TTL) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 178

def cached_mesh_profile(agent_id:, ttl: MESH_CACHE_TTL)
  @mesh_cache ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
  entry = @mesh_cache[agent_id.to_s] # rubocop:disable ThreadSafety/ClassInstanceVariable
  return nil unless entry

  if Time.now - entry[:cached_at] > ttl
    @mesh_cache.delete(agent_id.to_s) # rubocop:disable ThreadSafety/ClassInstanceVariable
    return nil
  end

  entry[:profile]
rescue StandardError => _e
  nil
end

.clear_mesh_cache(agent_id: nil) ⇒ Object



193
194
195
196
197
198
199
200
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 193

def clear_mesh_cache(agent_id: nil)
  @mesh_cache ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
  if agent_id
    @mesh_cache.delete(agent_id.to_s) # rubocop:disable ThreadSafety/ClassInstanceVariable
  else
    @mesh_cache.clear # rubocop:disable ThreadSafety/ClassInstanceVariable
  end
end

.clear_observationsObject



306
307
308
309
310
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 306

def clear_observations
  @observation_counts    = {} # rubocop:disable ThreadSafety/ClassInstanceVariable
  @observation_signals   = {} # rubocop:disable ThreadSafety/ClassInstanceVariable
  @inferred_preferences  = {} # rubocop:disable ThreadSafety/ClassInstanceVariable
end

.clear_preferences(owner_id:, source: nil) ⇒ Object



103
104
105
106
107
108
109
110
111
112
113
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 103

def clear_preferences(owner_id:, source: nil)
  return { cleared: false, reason: :memory_not_available } unless memory_available?

  runner = memory_runner
  result = runner.retrieve_by_domain(domain_tag: "owner:#{owner_id}")
  traces = result[:traces] || []
  traces.each { |t| runner.delete_trace(trace_id: t[:trace_id]) }
  { cleared: true, owner_id: owner_id, source: source, count: traces.size }
rescue StandardError => e
  { cleared: false, reason: :error, message: e.message }
end

.compute_compatibility(profile) ⇒ Object



262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 262

def compute_compatibility(profile)
  return nil unless profile.is_a?(Hash) && profile[:personality]
  return nil unless personality_available?

  personality_runner = Object.new.extend(
    Legion::Extensions::Agentic::Self::Personality::Runners::Personality
  )
  result = personality_runner.personality_compatibility(other_profile: profile[:personality])
  { score: result[:compatibility], interpretation: result[:interpretation] }
rescue StandardError => _e
  nil
end

.derive_and_store_observations(owner_id:) ⇒ Object



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 312

def derive_and_store_observations(owner_id:)
  @observation_signals ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
  signals = @observation_signals[owner_id] || [] # rubocop:disable ThreadSafety/ClassInstanceVariable
  return if signals.empty?

  inferred = []
  inferred << derive_verbosity(signals)
  inferred << derive_tone(signals)
  inferred << derive_format(signals)
  inferred = inferred.compact

  @inferred_preferences ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
  @inferred_preferences[owner_id] = inferred # rubocop:disable ThreadSafety/ClassInstanceVariable

  inferred.each do |pref|
    store_preference(
      owner_id: owner_id,
      domain:   pref[:domain],
      value:    pref[:value],
      source:   pref[:source]
    )
  end
end

.derive_format(signals) ⇒ Object



352
353
354
355
356
357
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 352

def derive_format(signals)
  unique_channels = signals.map { |s| s[:channel] }.uniq
  return nil unless unique_channels.size >= 3

  { domain: 'format', value: 'adaptive', source: 'observation', confidence: 0.55 }
end

.derive_tone(signals) ⇒ Object



344
345
346
347
348
349
350
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 344

def derive_tone(signals)
  da_count = signals.count { |s| s[:direct_address] }
  da_ratio = da_count.to_f / signals.size
  return nil unless da_ratio >= 0.5

  { domain: 'tone', value: 'conversational', source: 'observation', confidence: 0.65 }
end

.derive_verbosity(signals) ⇒ Object



336
337
338
339
340
341
342
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 336

def derive_verbosity(signals)
  cli_count = signals.count { |s| s[:channel] == :cli }
  cli_ratio = cli_count.to_f / signals.size
  return nil unless cli_ratio >= 0.5

  { domain: 'verbosity', value: 'concise', source: 'observation', confidence: 0.65 }
end

.erase_partner!(identity:) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 115

def erase_partner!(identity:)
  key = identity.to_s
  @observation_counts&.delete(key)    # rubocop:disable ThreadSafety/ClassInstanceVariable
  @observation_signals&.delete(key)   # rubocop:disable ThreadSafety/ClassInstanceVariable
  @inferred_preferences&.delete(key)  # rubocop:disable ThreadSafety/ClassInstanceVariable
  clear_mesh_cache(agent_id: key)

  traces_deleted = 0
  if memory_available?
    runner = memory_runner
    result = runner.retrieve_by_domain(domain_tag: "owner:#{key}")
    traces = result[:traces] || []
    traces.each { |t| runner.delete_trace(trace_id: t[:trace_id]) }
    traces_deleted = traces.size
  end

  { erased: true, identity: key, traces_deleted: traces_deleted }
rescue StandardError => e
  { erased: false, identity: key, reason: :error, message: e.message }
end

.fetch_preference_traces(owner_id:) ⇒ Object



239
240
241
242
243
244
245
246
247
248
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 239

def fetch_preference_traces(owner_id:)
  return [] unless memory_available?

  traces = memory_runner.retrieve_by_domain(domain_tag: "owner:#{owner_id}")
  traces.select { |t| t[:domain_tags]&.include?('preference') }.filter_map do |trace|
    parse_preference_trace(trace)
  end
rescue StandardError => _e
  []
end

.for_agent(agent_id:, ttl: MESH_CACHE_TTL) ⇒ Object



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 202

def for_agent(agent_id:, ttl: MESH_CACHE_TTL)
  cached = cached_mesh_profile(agent_id: agent_id, ttl: ttl)
  if cached
    return {
      source:        :mesh_cache,
      profile:       cached,
      agent_id:      agent_id,
      compatibility: compute_compatibility(cached)
    }
  end

  profile = resolve(owner_id: agent_id)

  {
    source:        :local,
    profile:       profile,
    agent_id:      agent_id,
    compatibility: compute_compatibility(profile)
  }
rescue StandardError => e
  {
    source:        :local,
    profile:       DEFAULTS.dup,
    agent_id:      agent_id,
    compatibility: nil,
    error:         e.message
  }
end

.inferred_preferences(owner_id) ⇒ Object



301
302
303
304
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 301

def inferred_preferences(owner_id)
  @inferred_preferences ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
  @inferred_preferences[owner_id.to_s] || [] # rubocop:disable ThreadSafety/ClassInstanceVariable
end

.memory_available?Boolean

Returns:

  • (Boolean)


231
232
233
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 231

def memory_available?
  defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
end

.memory_runnerObject



235
236
237
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 235

def memory_runner
  Object.new.extend(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
end

.observation_countsObject



297
298
299
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 297

def observation_counts
  @observation_counts ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
end

.parse_preference_trace(trace) ⇒ Object



250
251
252
253
254
255
256
257
258
259
260
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 250

def parse_preference_trace(trace)
  payload = trace[:content_payload]
  return nil unless payload.is_a?(String)

  match = payload.match(/domain.*?(\w+).*?value.*?(\w+).*?source.*?(\w+)/)
  return nil unless match

  { domain: match[1], value: match[2], source: match[3], confidence: trace[:confidence] }
rescue StandardError => _e
  nil
end

.personality_available?Boolean

Returns:

  • (Boolean)


275
276
277
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 275

def personality_available?
  defined?(Legion::Extensions::Agentic::Self::Personality::Runners::Personality)
end

.preference_instructions(profile:) ⇒ Object



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 136

def preference_instructions(profile:)
  lines = []

  INSTRUCTION_MAP.each do |domain, value_map|
    current = profile[domain]
    default = DEFAULTS[domain]
    next if current == default

    instruction = value_map[current]
    lines << instruction if instruction
  end

  return nil if lines.empty?

  lines.join(' ')
end

.resolve(owner_id:, overrides: nil, personality: nil) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 56

def resolve(owner_id:, overrides: nil, personality: nil)
  prefs = DEFAULTS.dup
  sources = Set.new([:defaults])
  custom = {}

  traces = fetch_preference_traces(owner_id: owner_id)
  all_prefs = traces + (overrides || [])

  all_prefs.sort_by { |p| p[:confidence] || SOURCE_CONFIDENCE.fetch(p[:source].to_s, 0.0) }.each do |pref|
    domain = pref[:domain].to_s
    value = pref[:value].to_s
    source = pref[:source].to_s

    if domain.start_with?('custom:')
      custom_key = domain.sub('custom:', '')
      custom[custom_key] = value
      sources << source.to_sym
    elsif VALID_VALUES.key?(domain) && VALID_VALUES[domain].include?(value.to_sym)
      prefs[domain.to_sym] = value.to_sym
      sources << source.to_sym
    end
  end

  prefs.merge(
    personality: personality,
    custom:      custom,
    sources:     sources.to_a,
    resolved_at: Time.now
  )
end

.store_mesh_profile(agent_id:, profile:, source_agent_id:) ⇒ Object



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 155

def store_mesh_profile(agent_id:, profile:, source_agent_id:)
  @mesh_cache ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
  @mesh_cache[agent_id.to_s] = { # rubocop:disable ThreadSafety/ClassInstanceVariable
    profile:         profile,
    source_agent_id: source_agent_id,
    origin:          :mesh_transfer,
    cached_at:       Time.now
  }

  if memory_available?
    store_preference(
      owner_id: agent_id,
      domain:   'mesh_profile',
      value:    profile.to_s,
      source:   'mesh_transfer'
    )
  end

  { stored: true, agent_id: agent_id, origin: :mesh_transfer }
rescue StandardError => e
  { stored: false, error: e.message }
end

.store_preference(owner_id:, domain:, value:, source: 'explicit') ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 87

def store_preference(owner_id:, domain:, value:, source: 'explicit')
  return { stored: false, reason: :memory_not_available } unless memory_available?

  confidence = SOURCE_CONFIDENCE.fetch(source.to_s, 0.5)
  memory_runner.store_trace(
    type:            :semantic,
    content_payload: { domain: domain, value: value, source: source }.to_s,
    domain_tags:     ['preference', "preference:#{domain}", "owner:#{owner_id}"],
    origin:          :direct_experience,
    confidence:      confidence
  )
  { stored: true, domain: domain, value: value, source: source }
rescue StandardError => e
  { stored: false, reason: :error, message: e.message }
end

.update_from_observation(owner_id:, signals:) ⇒ Object



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/legion/extensions/mesh/helpers/preference_profile.rb', line 281

def update_from_observation(owner_id:, signals:)
  @observation_counts  ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable
  @observation_signals ||= {} # rubocop:disable ThreadSafety/ClassInstanceVariable

  key = owner_id.to_s
  @observation_counts[key] = (@observation_counts[key] || 0) + 1 # rubocop:disable ThreadSafety/ClassInstanceVariable
  @observation_signals[key] ||= [] # rubocop:disable ThreadSafety/ClassInstanceVariable
  @observation_signals[key] << signals # rubocop:disable ThreadSafety/ClassInstanceVariable

  derive_and_store_observations(owner_id: key) if @observation_counts[key] >= OBSERVATION_THRESHOLD # rubocop:disable ThreadSafety/ClassInstanceVariable

  { updated: true }
rescue StandardError => _e
  { updated: false }
end