lex-personality 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 900b7ca389f94db331c1959497b8d65678c85e81052e4cab54c1871f436ba638
4
+ data.tar.gz: 724436bf9d1e2e5d476b76c822097964ce75f92db4e2f9f1315e119472b13c9a
5
+ SHA512:
6
+ metadata.gz: 5579277a9f55a11a0bec90b5744e2a2695ad37c2e115b35f43eeb065bacee8e704e8570b8c47b0594c49ccc6ead158cc360680a43acb4853c8d40cc4842e1971
7
+ data.tar.gz: 57498f12959d05594f69021b638f45a46269f309fd5fcec0c186a775af14faf127d490cbff86ca64d4d5aa31817f179d9a915814c6e9279c025690b3b793ea3e
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Personality
6
+ class Client
7
+ include Runners::Personality
8
+
9
+ attr_reader :personality_store
10
+
11
+ def initialize(personality_store: nil, **)
12
+ @personality_store = personality_store || Helpers::PersonalityStore.new
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Personality
6
+ module Helpers
7
+ module Constants
8
+ # The Big Five personality dimensions (OCEAN)
9
+ TRAITS = %i[openness conscientiousness extraversion agreeableness neuroticism].freeze
10
+
11
+ # Starting values — neutral personality (0.5 = average on each trait)
12
+ DEFAULT_TRAIT_VALUE = 0.5
13
+
14
+ # EMA alpha for trait updates — very slow (personality is stable)
15
+ TRAIT_ALPHA = 0.02
16
+
17
+ # Minimum observations before personality is considered "formed"
18
+ FORMATION_THRESHOLD = 100
19
+
20
+ # Maximum trait history entries
21
+ MAX_HISTORY = 200
22
+
23
+ # Trait descriptors for each level
24
+ TRAIT_DESCRIPTORS = {
25
+ openness: {
26
+ high: 'highly curious and open to new experiences',
27
+ mid: 'moderately exploratory',
28
+ low: 'preferring familiar approaches and known domains'
29
+ },
30
+ conscientiousness: {
31
+ high: 'very reliable, consistent, and detail-oriented',
32
+ mid: 'reasonably organized and dependable',
33
+ low: 'flexible but sometimes inconsistent'
34
+ },
35
+ extraversion: {
36
+ high: 'socially engaged, frequently communicating with other agents',
37
+ mid: 'balanced between collaboration and independent work',
38
+ low: 'preferring independent work with minimal social interaction'
39
+ },
40
+ agreeableness: {
41
+ high: 'cooperative, trusting, and conflict-averse',
42
+ mid: 'balanced between cooperation and independent judgment',
43
+ low: 'assertive and willing to challenge others'
44
+ },
45
+ neuroticism: {
46
+ high: 'emotionally sensitive, prone to anxiety under stress',
47
+ mid: 'moderately resilient to emotional fluctuation',
48
+ low: 'emotionally stable and stress-resistant'
49
+ }
50
+ }.freeze
51
+
52
+ # Signal extraction: maps tick_results keys to trait influences
53
+ # Each entry: [trait, direction, weight]
54
+ # direction: :positive means high signal increases trait, :negative means high signal decreases it
55
+ SIGNAL_MAP = {
56
+ curiosity_intensity: [:openness, :positive, 0.3],
57
+ novel_domains: [:openness, :positive, 0.4],
58
+ prediction_accuracy: [:conscientiousness, :positive, 0.3],
59
+ habit_automatic_ratio: [:conscientiousness, :positive, 0.3],
60
+ error_rate: [:conscientiousness, :negative, 0.4],
61
+ mesh_message_count: [:extraversion, :positive, 0.4],
62
+ empathy_model_count: [:extraversion, :positive, 0.3],
63
+ cooperation_ratio: [:agreeableness, :positive, 0.4],
64
+ trust_extension_rate: [:agreeableness, :positive, 0.3],
65
+ conflict_frequency: [:agreeableness, :negative, 0.3],
66
+ emotional_volatility: [:neuroticism, :positive, 0.4],
67
+ anxiety_frequency: [:neuroticism, :positive, 0.3],
68
+ mood_stability: [:neuroticism, :negative, 0.3]
69
+ }.freeze
70
+
71
+ # Threshold for "high" trait descriptor
72
+ HIGH_THRESHOLD = 0.65
73
+
74
+ # Threshold for "low" trait descriptor
75
+ LOW_THRESHOLD = 0.35
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Personality
6
+ module Helpers
7
+ class PersonalityStore
8
+ attr_reader :model
9
+
10
+ def initialize
11
+ @model = TraitModel.new
12
+ end
13
+
14
+ def update(tick_results)
15
+ signals = extract_signals(tick_results)
16
+ @model.update(signals)
17
+ end
18
+
19
+ def full_description
20
+ return 'Personality not yet formed — insufficient observations.' unless @model.formed?
21
+
22
+ parts = Constants::TRAITS.filter_map do |trait|
23
+ @model.describe(trait)
24
+ end
25
+ "#{parts.join('. ')}."
26
+ end
27
+
28
+ def compatibility_score(other_profile)
29
+ return nil unless other_profile.is_a?(Hash)
30
+
31
+ diffs = Constants::TRAITS.map do |trait|
32
+ mine = @model.trait(trait) || 0.5
33
+ theirs = other_profile[trait] || 0.5
34
+ (mine - theirs).abs
35
+ end
36
+ avg_diff = diffs.sum / diffs.size.to_f
37
+ (1.0 - avg_diff).round(3)
38
+ end
39
+
40
+ private
41
+
42
+ def extract_signals(tick_results)
43
+ signals = {}
44
+
45
+ signals[:curiosity_intensity] = extract_curiosity(tick_results)
46
+ signals[:novel_domains] = extract_novelty(tick_results)
47
+ signals[:prediction_accuracy] = extract_prediction(tick_results)
48
+ signals[:error_rate] = extract_error_rate(tick_results)
49
+ signals[:mesh_message_count] = extract_mesh(tick_results)
50
+ signals[:empathy_model_count] = extract_empathy(tick_results)
51
+ signals[:cooperation_ratio] = extract_cooperation(tick_results)
52
+ signals[:conflict_frequency] = extract_conflict(tick_results)
53
+ signals[:emotional_volatility] = extract_volatility(tick_results)
54
+ signals[:mood_stability] = extract_mood_stability(tick_results)
55
+
56
+ signals.compact
57
+ end
58
+
59
+ def extract_curiosity(tick_results)
60
+ tick_results.dig(:working_memory_integration, :intensity) ||
61
+ tick_results.dig(:curiosity, :intensity)
62
+ end
63
+
64
+ def extract_novelty(tick_results)
65
+ count = tick_results.dig(:working_memory_integration, :wonder_count) ||
66
+ tick_results.dig(:curiosity, :wonder_count)
67
+ return nil unless count.is_a?(Numeric)
68
+
69
+ (count / 10.0).clamp(0.0, 1.0)
70
+ end
71
+
72
+ def extract_prediction(tick_results)
73
+ tick_results.dig(:prediction_engine, :accuracy) ||
74
+ tick_results.dig(:prediction, :accuracy)
75
+ end
76
+
77
+ def extract_error_rate(tick_results)
78
+ accuracy = extract_prediction(tick_results)
79
+ return nil unless accuracy.is_a?(Numeric)
80
+
81
+ 1.0 - accuracy
82
+ end
83
+
84
+ def extract_mesh(tick_results)
85
+ count = tick_results.dig(:mesh_interface, :message_count) ||
86
+ tick_results.dig(:mesh, :message_count)
87
+ return nil unless count.is_a?(Numeric)
88
+
89
+ (count / 20.0).clamp(0.0, 1.0)
90
+ end
91
+
92
+ def extract_empathy(tick_results)
93
+ count = tick_results.dig(:empathy, :model_count)
94
+ return nil unless count.is_a?(Numeric)
95
+
96
+ (count / 10.0).clamp(0.0, 1.0)
97
+ end
98
+
99
+ def extract_cooperation(tick_results)
100
+ tick_results.dig(:empathy, :cooperation_ratio)
101
+ end
102
+
103
+ def extract_conflict(tick_results)
104
+ count = tick_results.dig(:conflict, :active_count)
105
+ return nil unless count.is_a?(Numeric)
106
+
107
+ (count / 5.0).clamp(0.0, 1.0)
108
+ end
109
+
110
+ def extract_volatility(tick_results)
111
+ tick_results.dig(:emotional_evaluation, :volatility) ||
112
+ tick_results.dig(:emotion, :volatility)
113
+ end
114
+
115
+ def extract_mood_stability(tick_results)
116
+ tick_results.dig(:mood, :stability)
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Personality
6
+ module Helpers
7
+ class TraitModel
8
+ attr_reader :traits, :observation_count, :history
9
+
10
+ def initialize
11
+ @traits = Constants::TRAITS.to_h do |trait|
12
+ [trait, Constants::DEFAULT_TRAIT_VALUE]
13
+ end
14
+ @observation_count = 0
15
+ @history = []
16
+ end
17
+
18
+ def update(signals)
19
+ observations = extract_observations(signals)
20
+ return if observations.empty?
21
+
22
+ apply_observations(observations)
23
+ @observation_count += 1
24
+ record_snapshot
25
+ end
26
+
27
+ def trait(name)
28
+ @traits[name.to_sym]
29
+ end
30
+
31
+ def formed?
32
+ @observation_count >= Constants::FORMATION_THRESHOLD
33
+ end
34
+
35
+ def dominant_trait
36
+ @traits.max_by { |_k, v| (v - 0.5).abs }&.first
37
+ end
38
+
39
+ def trait_level(name)
40
+ value = @traits[name.to_sym]
41
+ return nil unless value
42
+
43
+ if value >= Constants::HIGH_THRESHOLD
44
+ :high
45
+ elsif value <= Constants::LOW_THRESHOLD
46
+ :low
47
+ else
48
+ :mid
49
+ end
50
+ end
51
+
52
+ def describe(name)
53
+ level = trait_level(name)
54
+ return nil unless level
55
+
56
+ Constants::TRAIT_DESCRIPTORS.dig(name.to_sym, level)
57
+ end
58
+
59
+ def profile
60
+ @traits.transform_values { |v| v.round(3) }
61
+ end
62
+
63
+ def stability
64
+ return 0.0 if @history.size < 5
65
+
66
+ recent = @history.last(10)
67
+ variances = Constants::TRAITS.map do |t|
68
+ values = recent.map { |h| h[:traits][t] }
69
+ mean = values.sum / values.size.to_f
70
+ values.map { |v| (v - mean)**2 }.sum / values.size.to_f
71
+ end
72
+ avg_variance = variances.sum / variances.size.to_f
73
+ (1.0 - (avg_variance * 10)).clamp(0.0, 1.0)
74
+ end
75
+
76
+ def trend(name)
77
+ return :insufficient_data if @history.size < 5
78
+
79
+ values = @history.last(10).map { |h| h[:traits][name.to_sym] }
80
+ first_half = values[0...(values.size / 2)]
81
+ second_half = values[(values.size / 2)..]
82
+ diff = (second_half.sum / second_half.size.to_f) - (first_half.sum / first_half.size.to_f)
83
+
84
+ if diff > 0.02
85
+ :increasing
86
+ elsif diff < -0.02
87
+ :decreasing
88
+ else
89
+ :stable
90
+ end
91
+ end
92
+
93
+ def to_h
94
+ {
95
+ traits: profile,
96
+ observation_count: @observation_count,
97
+ formed: formed?,
98
+ stability: stability,
99
+ dominant_trait: dominant_trait,
100
+ history_size: @history.size
101
+ }
102
+ end
103
+
104
+ private
105
+
106
+ def extract_observations(signals)
107
+ observations = Hash.new { |h, k| h[k] = [] }
108
+
109
+ Constants::SIGNAL_MAP.each do |signal_key, (trait, direction, weight)|
110
+ value = signals[signal_key]
111
+ next unless value.is_a?(Numeric)
112
+
113
+ effective = direction == :positive ? value : 1.0 - value
114
+ observations[trait] << { value: effective, weight: weight }
115
+ end
116
+
117
+ observations
118
+ end
119
+
120
+ def apply_observations(observations)
121
+ observations.each do |trait, obs_list|
122
+ weighted_sum = obs_list.sum { |o| o[:value] * o[:weight] }
123
+ total_weight = obs_list.sum { |o| o[:weight] }
124
+ next if total_weight.zero?
125
+
126
+ target = (weighted_sum / total_weight).clamp(0.0, 1.0)
127
+ @traits[trait] = ema(@traits[trait], target, Constants::TRAIT_ALPHA)
128
+ end
129
+ end
130
+
131
+ def ema(current, observed, alpha)
132
+ (current * (1.0 - alpha)) + (observed * alpha)
133
+ end
134
+
135
+ def record_snapshot
136
+ @history << { traits: @traits.dup, at: Time.now.utc }
137
+ @history.shift while @history.size > Constants::MAX_HISTORY
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Personality
6
+ module Runners
7
+ module Personality
8
+ include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
9
+
10
+ def update_personality(tick_results: {}, **)
11
+ personality_store.update(tick_results)
12
+ model = personality_store.model
13
+
14
+ {
15
+ updated: true,
16
+ observation_count: model.observation_count,
17
+ formed: model.formed?,
18
+ profile: model.profile,
19
+ dominant_trait: model.dominant_trait
20
+ }
21
+ end
22
+
23
+ def personality_profile(**)
24
+ model = personality_store.model
25
+ {
26
+ traits: model.profile,
27
+ formed: model.formed?,
28
+ stability: model.stability,
29
+ dominant: model.dominant_trait,
30
+ observations: model.observation_count,
31
+ history_size: model.history.size
32
+ }
33
+ end
34
+
35
+ def describe_personality(**)
36
+ {
37
+ description: personality_store.full_description,
38
+ formed: personality_store.model.formed?
39
+ }
40
+ end
41
+
42
+ def trait_detail(trait:, **)
43
+ model = personality_store.model
44
+ value = model.trait(trait)
45
+ return { trait: trait.to_sym, error: :unknown_trait } unless value
46
+
47
+ {
48
+ trait: trait.to_sym,
49
+ value: value.round(3),
50
+ level: model.trait_level(trait),
51
+ description: model.describe(trait),
52
+ trend: model.trend(trait)
53
+ }
54
+ end
55
+
56
+ def personality_compatibility(other_profile:, **)
57
+ score = personality_store.compatibility_score(other_profile)
58
+ {
59
+ compatibility: score,
60
+ interpretation: interpret_compatibility(score)
61
+ }
62
+ end
63
+
64
+ def personality_stats(**)
65
+ model = personality_store.model
66
+ {
67
+ observation_count: model.observation_count,
68
+ formed: model.formed?,
69
+ stability: model.stability,
70
+ dominant_trait: model.dominant_trait,
71
+ profile: model.profile,
72
+ history_size: model.history.size,
73
+ trait_trends: Helpers::Constants::TRAITS.to_h { |t| [t, model.trend(t)] }
74
+ }
75
+ end
76
+
77
+ private
78
+
79
+ def interpret_compatibility(score)
80
+ return :unknown unless score.is_a?(Numeric)
81
+
82
+ if score >= 0.85
83
+ :highly_compatible
84
+ elsif score >= 0.7
85
+ :compatible
86
+ elsif score >= 0.5
87
+ :neutral
88
+ elsif score >= 0.3
89
+ :divergent
90
+ else
91
+ :incompatible
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Personality
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'personality/version'
4
+ require_relative 'personality/helpers/constants'
5
+ require_relative 'personality/helpers/trait_model'
6
+ require_relative 'personality/helpers/personality_store'
7
+ require_relative 'personality/runners/personality'
8
+ require_relative 'personality/client'
9
+
10
+ module Legion
11
+ module Extensions
12
+ module Personality
13
+ extend Legion::Extensions::Core if defined?(Legion::Extensions::Core)
14
+ end
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-personality
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Iverson
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: legion-gaia
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: Big Five personality model emerging from accumulated cognitive behavior
27
+ patterns
28
+ email:
29
+ - matt@legionIO.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/legion/extensions/personality.rb
35
+ - lib/legion/extensions/personality/client.rb
36
+ - lib/legion/extensions/personality/helpers/constants.rb
37
+ - lib/legion/extensions/personality/helpers/personality_store.rb
38
+ - lib/legion/extensions/personality/helpers/trait_model.rb
39
+ - lib/legion/extensions/personality/runners/personality.rb
40
+ - lib/legion/extensions/personality/version.rb
41
+ homepage: https://github.com/LegionIO/lex-personality
42
+ licenses:
43
+ - MIT
44
+ metadata:
45
+ rubygems_mfa_required: 'true'
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3.4'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.6.9
61
+ specification_version: 4
62
+ summary: Emergent personality traits for LegionIO cognitive agents
63
+ test_files: []