lex-social 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: 4fd404736b85974600feaa1f44e7aaf419034288b9f8d243758d8700d492ac81
4
+ data.tar.gz: 5807d2f6fcb4ba8ec32add6876e44293dea24da7423868a4fecacc2d0803fc7a
5
+ SHA512:
6
+ metadata.gz: d6f1779cd6f358569921d73597b3764e9809115b68bc6096d5b320ac4f8b642f00292ba29fe1d8d225b7fdfb30a1b3754ccd492ef0e96144f6a5cd8bf2d65c8a
7
+ data.tar.gz: a358618950db736b213eb48173bbb898720df1c88857098a65f7adf2149d07ba420adadc4a57c2ebe43dcea02a62ea025cd9a7f0ab82393d34342cb250990704
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/social/helpers/constants'
4
+ require 'legion/extensions/social/helpers/social_graph'
5
+ require 'legion/extensions/social/runners/social'
6
+
7
+ module Legion
8
+ module Extensions
9
+ module Social
10
+ class Client
11
+ include Runners::Social
12
+
13
+ attr_reader :social_graph
14
+
15
+ def initialize(social_graph: nil, **)
16
+ @social_graph = social_graph || Helpers::SocialGraph.new
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Social
6
+ module Helpers
7
+ module Constants
8
+ # Social roles an agent can hold in a group
9
+ ROLES = %i[
10
+ leader
11
+ contributor
12
+ specialist
13
+ observer
14
+ mentor
15
+ newcomer
16
+ ].freeze
17
+
18
+ # Group relationship types
19
+ RELATIONSHIP_TYPES = %i[
20
+ ally
21
+ collaborator
22
+ neutral
23
+ competitor
24
+ adversary
25
+ ].freeze
26
+
27
+ # Reputation dimensions
28
+ REPUTATION_DIMENSIONS = {
29
+ reliability: { weight: 0.25, description: 'Follows through on commitments' },
30
+ competence: { weight: 0.25, description: 'Quality of work output' },
31
+ benevolence: { weight: 0.20, description: 'Acts in others interests' },
32
+ integrity: { weight: 0.15, description: 'Consistency between words and actions' },
33
+ influence: { weight: 0.15, description: 'Ability to shape group direction' }
34
+ }.freeze
35
+
36
+ # EMA alpha for reputation updates
37
+ REPUTATION_ALPHA = 0.1
38
+
39
+ # Social standing thresholds
40
+ STANDING_LEVELS = {
41
+ exemplary: 0.8,
42
+ respected: 0.6,
43
+ neutral: 0.4,
44
+ marginal: 0.2,
45
+ ostracized: 0.0
46
+ }.freeze
47
+
48
+ # Group cohesion thresholds
49
+ COHESION_LEVELS = {
50
+ tight: 0.7,
51
+ moderate: 0.5,
52
+ loose: 0.3,
53
+ fractured: 0.0
54
+ }.freeze
55
+
56
+ # Maximum tracked groups
57
+ MAX_GROUPS = 20
58
+
59
+ # Maximum members per group
60
+ MAX_GROUP_MEMBERS = 50
61
+
62
+ # Social norms violation types
63
+ NORM_VIOLATIONS = %i[
64
+ free_riding
65
+ defection
66
+ deception
67
+ dominance_abuse
68
+ exclusion
69
+ ].freeze
70
+
71
+ # Reciprocity tracking window
72
+ RECIPROCITY_WINDOW = 50
73
+
74
+ # Social influence decay rate
75
+ INFLUENCE_DECAY = 0.02
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Social
6
+ module Helpers
7
+ class SocialGraph
8
+ attr_reader :groups, :reputation_scores, :reciprocity_ledger
9
+
10
+ def initialize
11
+ @groups = {}
12
+ @reputation_scores = {}
13
+ @reciprocity_ledger = []
14
+ end
15
+
16
+ def join_group(group_id:, role: :contributor, members: [])
17
+ @groups[group_id] ||= {
18
+ role: role,
19
+ members: members.dup,
20
+ joined_at: Time.now.utc,
21
+ norms: [],
22
+ cohesion: 0.5,
23
+ violations: []
24
+ }
25
+ trim_groups
26
+ @groups[group_id]
27
+ end
28
+
29
+ def leave_group(group_id)
30
+ @groups.delete(group_id)
31
+ end
32
+
33
+ def update_role(group_id:, role:)
34
+ return nil unless @groups.key?(group_id)
35
+ return nil unless Constants::ROLES.include?(role)
36
+
37
+ @groups[group_id][:role] = role
38
+ end
39
+
40
+ def update_reputation(agent_id:, dimension:, signal:)
41
+ return nil unless Constants::REPUTATION_DIMENSIONS.key?(dimension)
42
+
43
+ @reputation_scores[agent_id] ||= Constants::REPUTATION_DIMENSIONS.keys.to_h { |d| [d, 0.5] }
44
+ current = @reputation_scores[agent_id][dimension]
45
+ @reputation_scores[agent_id][dimension] = ema(current, signal.clamp(0.0, 1.0), Constants::REPUTATION_ALPHA)
46
+ end
47
+
48
+ def reputation_for(agent_id)
49
+ scores = @reputation_scores[agent_id]
50
+ return nil unless scores
51
+
52
+ composite = 0.0
53
+ Constants::REPUTATION_DIMENSIONS.each do |dim, config|
54
+ composite += scores[dim] * config[:weight]
55
+ end
56
+
57
+ {
58
+ agent_id: agent_id,
59
+ scores: scores.transform_values { |v| v.round(4) },
60
+ composite: composite.round(4),
61
+ standing: classify_standing(composite)
62
+ }
63
+ end
64
+
65
+ def social_standing
66
+ return :neutral if @reputation_scores.empty?
67
+
68
+ all_composites = @reputation_scores.map { |id, _| reputation_for(id)[:composite] }
69
+ avg = all_composites.sum / all_composites.size.to_f
70
+ classify_standing(avg)
71
+ end
72
+
73
+ def record_reciprocity(agent_id:, action:, direction:)
74
+ @reciprocity_ledger << {
75
+ agent_id: agent_id,
76
+ action: action,
77
+ direction: direction,
78
+ at: Time.now.utc
79
+ }
80
+ @reciprocity_ledger.shift while @reciprocity_ledger.size > Constants::RECIPROCITY_WINDOW
81
+ end
82
+
83
+ def reciprocity_balance(agent_id)
84
+ entries = @reciprocity_ledger.select { |e| e[:agent_id] == agent_id }
85
+ given = entries.count { |e| e[:direction] == :given }
86
+ received = entries.count { |e| e[:direction] == :received }
87
+
88
+ { given: given, received: received, balance: given - received }
89
+ end
90
+
91
+ def record_violation(group_id:, type:, agent_id:)
92
+ return nil unless @groups.key?(group_id)
93
+ return nil unless Constants::NORM_VIOLATIONS.include?(type)
94
+
95
+ violation = { type: type, agent_id: agent_id, at: Time.now.utc }
96
+ @groups[group_id][:violations] << violation
97
+ reduce_cohesion(group_id, 0.1)
98
+ violation
99
+ end
100
+
101
+ def group_cohesion(group_id)
102
+ return nil unless @groups.key?(group_id)
103
+
104
+ @groups[group_id][:cohesion]
105
+ end
106
+
107
+ def update_cohesion(group_id:, signal:)
108
+ return nil unless @groups.key?(group_id)
109
+
110
+ current = @groups[group_id][:cohesion]
111
+ @groups[group_id][:cohesion] = ema(current, signal.clamp(0.0, 1.0), Constants::REPUTATION_ALPHA)
112
+ end
113
+
114
+ def classify_cohesion(group_id)
115
+ cohesion = group_cohesion(group_id)
116
+ return nil unless cohesion
117
+
118
+ Constants::COHESION_LEVELS.each do |level, threshold|
119
+ return level if cohesion >= threshold
120
+ end
121
+ :fractured
122
+ end
123
+
124
+ def group_count
125
+ @groups.size
126
+ end
127
+
128
+ def agents_tracked
129
+ @reputation_scores.keys.size
130
+ end
131
+
132
+ def to_h
133
+ {
134
+ groups: @groups.keys,
135
+ group_count: @groups.size,
136
+ agents_tracked: agents_tracked,
137
+ social_standing: social_standing,
138
+ ledger_size: @reciprocity_ledger.size
139
+ }
140
+ end
141
+
142
+ private
143
+
144
+ def ema(current, observed, alpha)
145
+ (current * (1.0 - alpha)) + (observed * alpha)
146
+ end
147
+
148
+ def classify_standing(composite)
149
+ Constants::STANDING_LEVELS.each do |level, threshold|
150
+ return level if composite >= threshold
151
+ end
152
+ :ostracized
153
+ end
154
+
155
+ def reduce_cohesion(group_id, amount)
156
+ current = @groups[group_id][:cohesion]
157
+ @groups[group_id][:cohesion] = [current - amount, 0.0].max
158
+ end
159
+
160
+ def trim_groups
161
+ oldest = @groups.keys.sort_by { |k| @groups[k][:joined_at] }
162
+ oldest.first([@groups.size - Constants::MAX_GROUPS, 0].max).each { |k| @groups.delete(k) }
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Social
6
+ module Runners
7
+ module Social
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def update_social(tick_results: {}, **)
12
+ extract_social_signals(tick_results)
13
+
14
+ Legion::Logging.debug "[social] groups=#{social_graph.group_count} " \
15
+ "agents=#{social_graph.agents_tracked} standing=#{social_graph.social_standing}"
16
+
17
+ {
18
+ groups: social_graph.group_count,
19
+ agents_tracked: social_graph.agents_tracked,
20
+ standing: social_graph.social_standing,
21
+ ledger_size: social_graph.reciprocity_ledger.size
22
+ }
23
+ end
24
+
25
+ def join_group(group_id:, role: :contributor, members: [], **)
26
+ group = social_graph.join_group(group_id: group_id, role: role, members: members)
27
+ Legion::Logging.info "[social] joined group=#{group_id} role=#{role}"
28
+ { success: true, group_id: group_id, role: role, group: group }
29
+ end
30
+
31
+ def leave_group(group_id:, **)
32
+ social_graph.leave_group(group_id)
33
+ Legion::Logging.info "[social] left group=#{group_id}"
34
+ { success: true, group_id: group_id }
35
+ end
36
+
37
+ def update_reputation(agent_id:, dimension:, signal:, **)
38
+ result = social_graph.update_reputation(agent_id: agent_id, dimension: dimension.to_sym, signal: signal)
39
+ return { success: false, error: 'invalid dimension' } unless result
40
+
41
+ rep = social_graph.reputation_for(agent_id)
42
+ Legion::Logging.debug "[social] reputation updated agent=#{agent_id} dim=#{dimension}"
43
+ { success: true, reputation: rep }
44
+ end
45
+
46
+ def agent_reputation(agent_id:, **)
47
+ rep = social_graph.reputation_for(agent_id)
48
+ return { error: 'unknown agent' } unless rep
49
+
50
+ Legion::Logging.debug "[social] reputation for #{agent_id}: #{rep[:composite]}"
51
+ rep
52
+ end
53
+
54
+ def reciprocity_status(agent_id:, **)
55
+ balance = social_graph.reciprocity_balance(agent_id)
56
+ Legion::Logging.debug "[social] reciprocity #{agent_id}: #{balance}"
57
+ balance
58
+ end
59
+
60
+ def record_exchange(agent_id:, action:, direction:, **)
61
+ social_graph.record_reciprocity(agent_id: agent_id, action: action, direction: direction.to_sym)
62
+ Legion::Logging.debug "[social] exchange agent=#{agent_id} dir=#{direction}"
63
+ { success: true }
64
+ end
65
+
66
+ def report_violation(group_id:, type:, agent_id:, **)
67
+ violation = social_graph.record_violation(group_id: group_id, type: type.to_sym, agent_id: agent_id)
68
+ return { success: false, error: 'invalid group or violation type' } unless violation
69
+
70
+ Legion::Logging.warn "[social] violation: #{type} by #{agent_id} in #{group_id}"
71
+ { success: true, violation: violation, cohesion: social_graph.group_cohesion(group_id)&.round(4) }
72
+ end
73
+
74
+ def group_status(group_id:, **)
75
+ return { error: 'unknown group' } unless social_graph.groups.key?(group_id)
76
+
77
+ group = social_graph.groups[group_id]
78
+ {
79
+ group_id: group_id,
80
+ role: group[:role],
81
+ members: group[:members].size,
82
+ cohesion: group[:cohesion].round(4),
83
+ cohesion_level: social_graph.classify_cohesion(group_id),
84
+ violations: group[:violations].size
85
+ }
86
+ end
87
+
88
+ def social_status(**)
89
+ state = social_graph.to_h
90
+ Legion::Logging.debug "[social] status: #{state[:social_standing]}"
91
+ state
92
+ end
93
+
94
+ def social_stats(**)
95
+ Legion::Logging.debug '[social] stats'
96
+
97
+ {
98
+ groups: social_graph.group_count,
99
+ agents_tracked: social_graph.agents_tracked,
100
+ standing: social_graph.social_standing,
101
+ ledger_size: social_graph.reciprocity_ledger.size,
102
+ group_roles: social_graph.groups.transform_values { |g| g[:role] }
103
+ }
104
+ end
105
+
106
+ private
107
+
108
+ def social_graph
109
+ @social_graph ||= Helpers::SocialGraph.new
110
+ end
111
+
112
+ def extract_social_signals(tick_results)
113
+ extract_trust_signals(tick_results)
114
+ extract_mesh_signals(tick_results)
115
+ end
116
+
117
+ def extract_trust_signals(tick_results)
118
+ trust_updates = tick_results.dig(:trust, :updates)
119
+ return unless trust_updates.is_a?(Array)
120
+
121
+ trust_updates.each do |update|
122
+ social_graph.update_reputation(
123
+ agent_id: update[:agent_id],
124
+ dimension: :reliability,
125
+ signal: update[:score] || 0.5
126
+ )
127
+ end
128
+ end
129
+
130
+ def extract_mesh_signals(tick_results)
131
+ peer_count = tick_results.dig(:mesh_interface, :peer_count) || 0
132
+ return if peer_count.zero?
133
+
134
+ social_graph.groups.each_key do |group_id|
135
+ social_graph.update_cohesion(group_id: group_id, signal: [peer_count * 0.1, 1.0].min)
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Social
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/social/version'
4
+ require 'legion/extensions/social/helpers/constants'
5
+ require 'legion/extensions/social/helpers/social_graph'
6
+ require 'legion/extensions/social/runners/social'
7
+ require 'legion/extensions/social/client'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module Social
12
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined?(:Core)
13
+ end
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-social
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: Models social roles, group membership, reputation, and collective behavior
27
+ in multi-agent systems
28
+ email:
29
+ - matt@legionIO.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/legion/extensions/social.rb
35
+ - lib/legion/extensions/social/client.rb
36
+ - lib/legion/extensions/social/helpers/constants.rb
37
+ - lib/legion/extensions/social/helpers/social_graph.rb
38
+ - lib/legion/extensions/social/runners/social.rb
39
+ - lib/legion/extensions/social/version.rb
40
+ homepage: https://github.com/LegionIO/lex-social
41
+ licenses:
42
+ - MIT
43
+ metadata:
44
+ rubygems_mfa_required: 'true'
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '3.4'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubygems_version: 3.6.9
60
+ specification_version: 4
61
+ summary: Social identity and group dynamics for LegionIO cognitive agents
62
+ test_files: []