lex-agentic-social 0.1.7 → 0.1.8
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 +4 -4
- data/CHANGELOG.md +8 -0
- data/Gemfile +2 -0
- data/lex-agentic-social.gemspec +3 -3
- data/lib/legion/extensions/agentic/social/attachment/client.rb +15 -0
- data/lib/legion/extensions/agentic/social/attachment/helpers/attachment_model.rb +102 -0
- data/lib/legion/extensions/agentic/social/attachment/helpers/attachment_store.rb +87 -0
- data/lib/legion/extensions/agentic/social/attachment/helpers/constants.rb +55 -0
- data/lib/legion/extensions/agentic/social/attachment/runners/attachment.rb +169 -0
- data/lib/legion/extensions/agentic/social/attachment/version.rb +13 -0
- data/lib/legion/extensions/agentic/social/attachment.rb +8 -0
- data/lib/legion/extensions/agentic/social/version.rb +1 -1
- data/lib/legion/extensions/agentic/social.rb +1 -0
- data/spec/legion/extensions/agentic/social/attachment/helpers/attachment_model_spec.rb +144 -0
- data/spec/legion/extensions/agentic/social/attachment/helpers/attachment_store_spec.rb +121 -0
- data/spec/legion/extensions/agentic/social/attachment/helpers/constants_spec.rb +53 -0
- data/spec/legion/extensions/agentic/social/attachment/runners/attachment_spec.rb +130 -0
- metadata +24 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b5652d5875cf5aae7e51adf8b8b09f0e664a719144a7575ac9eb2f4bd7b7b56
|
|
4
|
+
data.tar.gz: 62101c073d3970708e278b2e7afd1dbbe3efc582eb03246bf19f975ad1999bd4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5e210bfd1a546461a34290e84e49a893cbb792c1d304b2087c759d2345a0cc30ac37c3f15760024ab19fdf83106f7cdefb87fa83c025100fd36852931f60c82f
|
|
7
|
+
data.tar.gz: 7a49e748bfc08d1c918c494f16d8ec4f579e63b3ec97120b532d168ffc9865e1748c9113025c0dfe7c9505c0f1aa3830624299e16926bc1b60be52187a71b0a5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.8] - 2026-03-31
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Attachment sub-module for Phase C bond modeling
|
|
7
|
+
- `AttachmentModel`: per-agent attachment strength, style, and stage tracking with EMA updates and no-regression stage transitions
|
|
8
|
+
- `AttachmentStore`: in-memory store with dirty tracking and Apollo Local persistence (`to_apollo_entries`, `from_apollo`)
|
|
9
|
+
- `Attachment` runner: `update_attachment` (tick integration), `reflect_on_bonds` (dream cycle orchestrator reading cross-module bond data), `attachment_stats`
|
|
10
|
+
|
|
3
11
|
## [0.1.7] - 2026-03-31
|
|
4
12
|
|
|
5
13
|
### Changed
|
data/Gemfile
CHANGED
data/lex-agentic-social.gemspec
CHANGED
|
@@ -33,7 +33,7 @@ Gem::Specification.new do |spec|
|
|
|
33
33
|
spec.add_dependency 'legion-transport', '>= 1.3.9'
|
|
34
34
|
|
|
35
35
|
spec.add_development_dependency 'rspec', '~> 3.13'
|
|
36
|
-
spec.add_development_dependency 'rubocop'
|
|
37
|
-
spec.add_development_dependency 'rubocop-legion'
|
|
38
|
-
spec.add_development_dependency 'rubocop-rspec'
|
|
36
|
+
spec.add_development_dependency 'rubocop'
|
|
37
|
+
spec.add_development_dependency 'rubocop-legion'
|
|
38
|
+
spec.add_development_dependency 'rubocop-rspec'
|
|
39
39
|
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Agentic
|
|
6
|
+
module Social
|
|
7
|
+
module Attachment
|
|
8
|
+
module Helpers
|
|
9
|
+
class AttachmentModel
|
|
10
|
+
attr_reader :agent_id, :attachment_strength, :attachment_style,
|
|
11
|
+
:bond_stage, :separation_tolerance, :interaction_count
|
|
12
|
+
|
|
13
|
+
def initialize(agent_id:)
|
|
14
|
+
@agent_id = agent_id
|
|
15
|
+
@attachment_strength = 0.0
|
|
16
|
+
@attachment_style = :secure
|
|
17
|
+
@bond_stage = :initial
|
|
18
|
+
@separation_tolerance = Constants::BASE_SEPARATION_TOLERANCE
|
|
19
|
+
@interaction_count = 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def update_from_signals(opts = {})
|
|
23
|
+
frequency_score = opts.fetch(:frequency_score, 0.0)
|
|
24
|
+
reciprocity_score = opts.fetch(:reciprocity_score, 0.0)
|
|
25
|
+
prediction_accuracy = opts.fetch(:prediction_accuracy, 0.0)
|
|
26
|
+
direct_address_ratio = opts.fetch(:direct_address_ratio, 0.0)
|
|
27
|
+
channel_consistency = opts.fetch(:channel_consistency, 0.0)
|
|
28
|
+
|
|
29
|
+
raw = (frequency_score * Constants::FREQUENCY_WEIGHT) +
|
|
30
|
+
(reciprocity_score * Constants::RECIPROCITY_WEIGHT) +
|
|
31
|
+
(prediction_accuracy * Constants::PREDICTION_ACCURACY_WEIGHT) +
|
|
32
|
+
(direct_address_ratio * Constants::DIRECT_ADDRESS_WEIGHT) +
|
|
33
|
+
(channel_consistency * Constants::CHANNEL_CONSISTENCY_WEIGHT)
|
|
34
|
+
|
|
35
|
+
@attachment_strength = if @interaction_count.zero?
|
|
36
|
+
raw.clamp(0.0, 1.0)
|
|
37
|
+
else
|
|
38
|
+
alpha = Constants::STRENGTH_ALPHA
|
|
39
|
+
((alpha * raw) + ((1.0 - alpha) * @attachment_strength)).clamp(0.0, 1.0)
|
|
40
|
+
end
|
|
41
|
+
@interaction_count += 1
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def update_stage!
|
|
45
|
+
new_stage = derive_stage
|
|
46
|
+
return if Constants::BOND_STAGES.index(new_stage) <= Constants::BOND_STAGES.index(@bond_stage)
|
|
47
|
+
|
|
48
|
+
@bond_stage = new_stage
|
|
49
|
+
@separation_tolerance = Constants::BASE_SEPARATION_TOLERANCE +
|
|
50
|
+
Constants::SEPARATION_TOLERANCE_GROWTH.fetch(@bond_stage, 0)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def derive_style!(opts = {})
|
|
54
|
+
frequency_variance = opts.fetch(:frequency_variance, 0.0)
|
|
55
|
+
reciprocity_imbalance = opts.fetch(:reciprocity_imbalance, 0.0)
|
|
56
|
+
frequency = opts.fetch(:frequency, 0.0)
|
|
57
|
+
direct_address_ratio = opts.fetch(:direct_address_ratio, 0.0)
|
|
58
|
+
thresholds = Constants::STYLE_THRESHOLDS
|
|
59
|
+
@attachment_style = if frequency_variance > thresholds[:anxious_frequency_variance] &&
|
|
60
|
+
reciprocity_imbalance > thresholds[:anxious_reciprocity_imbalance]
|
|
61
|
+
:anxious
|
|
62
|
+
elsif frequency < thresholds[:avoidant_frequency] &&
|
|
63
|
+
direct_address_ratio < thresholds[:avoidant_direct_address]
|
|
64
|
+
:avoidant
|
|
65
|
+
else
|
|
66
|
+
:secure
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def to_h
|
|
71
|
+
{ agent_id: @agent_id, attachment_strength: @attachment_strength,
|
|
72
|
+
attachment_style: @attachment_style, bond_stage: @bond_stage,
|
|
73
|
+
separation_tolerance: @separation_tolerance, interaction_count: @interaction_count }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.from_h(hash)
|
|
77
|
+
model = new(agent_id: hash[:agent_id])
|
|
78
|
+
model.instance_variable_set(:@attachment_strength, hash[:attachment_strength].to_f)
|
|
79
|
+
model.instance_variable_set(:@attachment_style, hash[:attachment_style]&.to_sym || :secure)
|
|
80
|
+
model.instance_variable_set(:@bond_stage, hash[:bond_stage]&.to_sym || :initial)
|
|
81
|
+
model.instance_variable_set(:@separation_tolerance, hash[:separation_tolerance]&.to_i || 3)
|
|
82
|
+
model.instance_variable_set(:@interaction_count, hash[:interaction_count].to_i)
|
|
83
|
+
model
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def derive_stage
|
|
89
|
+
Constants::STAGE_THRESHOLDS.each_key.reverse_each do |stage|
|
|
90
|
+
threshold = Constants::STAGE_THRESHOLDS[stage]
|
|
91
|
+
return stage if @interaction_count >= threshold[:interactions] &&
|
|
92
|
+
@attachment_strength >= threshold[:strength]
|
|
93
|
+
end
|
|
94
|
+
:initial
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Agentic
|
|
6
|
+
module Social
|
|
7
|
+
module Attachment
|
|
8
|
+
module Helpers
|
|
9
|
+
class AttachmentStore
|
|
10
|
+
def initialize
|
|
11
|
+
@models = {}
|
|
12
|
+
@dirty = false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get(agent_id)
|
|
16
|
+
@models[agent_id.to_s]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def get_or_create(agent_id)
|
|
20
|
+
key = agent_id.to_s
|
|
21
|
+
@models[key] ||= begin
|
|
22
|
+
@dirty = true
|
|
23
|
+
AttachmentModel.new(agent_id: key)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def all_models
|
|
28
|
+
@models.values
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def dirty?
|
|
32
|
+
@dirty
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def mark_clean!
|
|
36
|
+
@dirty = false
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_apollo_entries
|
|
41
|
+
@models.map do |agent_id, model|
|
|
42
|
+
tags = Constants::TAG_PREFIX.dup + [agent_id]
|
|
43
|
+
tags << 'partner' if partner?(agent_id)
|
|
44
|
+
content = serialize(model.to_h)
|
|
45
|
+
{ content: content, tags: tags }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def from_apollo(store:)
|
|
50
|
+
result = store.query(text: 'bond attachment', tags: %w[bond attachment])
|
|
51
|
+
return false unless result[:success] && result[:results]&.any?
|
|
52
|
+
|
|
53
|
+
result[:results].each do |entry|
|
|
54
|
+
parsed = deserialize(entry[:content])
|
|
55
|
+
next unless parsed && parsed[:agent_id]
|
|
56
|
+
|
|
57
|
+
@models[parsed[:agent_id].to_s] = AttachmentModel.from_h(parsed)
|
|
58
|
+
end
|
|
59
|
+
true
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
warn "[attachment_store] from_apollo error: #{e.message}"
|
|
62
|
+
false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def partner?(agent_id)
|
|
68
|
+
defined?(Legion::Gaia::BondRegistry) && Legion::Gaia::BondRegistry.partner?(agent_id)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def serialize(hash)
|
|
72
|
+
defined?(Legion::JSON) ? Legion::JSON.dump(hash) : ::JSON.dump(hash)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def deserialize(content)
|
|
76
|
+
parsed = defined?(Legion::JSON) ? Legion::JSON.parse(content) : ::JSON.parse(content, symbolize_names: true)
|
|
77
|
+
parsed.transform_keys(&:to_sym)
|
|
78
|
+
rescue StandardError => _e
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Agentic
|
|
6
|
+
module Social
|
|
7
|
+
module Attachment
|
|
8
|
+
module Helpers
|
|
9
|
+
module Constants
|
|
10
|
+
# Strength computation weights (must sum to 1.0)
|
|
11
|
+
FREQUENCY_WEIGHT = 0.3
|
|
12
|
+
RECIPROCITY_WEIGHT = 0.25
|
|
13
|
+
PREDICTION_ACCURACY_WEIGHT = 0.2
|
|
14
|
+
DIRECT_ADDRESS_WEIGHT = 0.15
|
|
15
|
+
CHANNEL_CONSISTENCY_WEIGHT = 0.1
|
|
16
|
+
|
|
17
|
+
# Bond lifecycle stages
|
|
18
|
+
BOND_STAGES = %i[initial forming established deep].freeze
|
|
19
|
+
|
|
20
|
+
# Attachment style classifications
|
|
21
|
+
ATTACHMENT_STYLES = %i[secure anxious avoidant].freeze
|
|
22
|
+
|
|
23
|
+
# Stage progression thresholds
|
|
24
|
+
STAGE_THRESHOLDS = {
|
|
25
|
+
forming: { interactions: 10, strength: 0.3 },
|
|
26
|
+
established: { interactions: 50, strength: 0.5 },
|
|
27
|
+
deep: { interactions: 200, strength: 0.7 }
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
# Separation tolerance (consecutive prediction misses before anxiety signal)
|
|
31
|
+
BASE_SEPARATION_TOLERANCE = 3
|
|
32
|
+
SEPARATION_TOLERANCE_GROWTH = {
|
|
33
|
+
initial: 0, forming: 1, established: 2, deep: 4
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
# EMA alpha for strength updates
|
|
37
|
+
STRENGTH_ALPHA = 0.15
|
|
38
|
+
|
|
39
|
+
# Style derivation thresholds
|
|
40
|
+
STYLE_THRESHOLDS = {
|
|
41
|
+
anxious_frequency_variance: 0.4,
|
|
42
|
+
anxious_reciprocity_imbalance: 0.3,
|
|
43
|
+
avoidant_frequency: 0.2,
|
|
44
|
+
avoidant_direct_address: 0.15
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
# Apollo Local tag prefix
|
|
48
|
+
TAG_PREFIX = %w[bond attachment].freeze
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Agentic
|
|
6
|
+
module Social
|
|
7
|
+
module Attachment
|
|
8
|
+
module Runners
|
|
9
|
+
module Attachment
|
|
10
|
+
include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers) &&
|
|
11
|
+
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
12
|
+
|
|
13
|
+
def update_attachment(tick_results: {}, human_observations: [], **)
|
|
14
|
+
agents = collect_agent_ids(tick_results, human_observations)
|
|
15
|
+
return { agents_updated: 0 } if agents.empty?
|
|
16
|
+
|
|
17
|
+
agents.each do |agent_id|
|
|
18
|
+
model = attachment_store.get_or_create(agent_id)
|
|
19
|
+
signals = extract_signals(agent_id, tick_results, human_observations)
|
|
20
|
+
model.update_from_signals(signals)
|
|
21
|
+
model.update_stage!
|
|
22
|
+
model.derive_style!(extract_style_signals(agent_id, human_observations))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
{ agents_updated: agents.size, models: agents.map { |id| attachment_store.get(id)&.to_h } }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def reflect_on_bonds(_tick_results: {}, _bond_summary: {}, **)
|
|
29
|
+
store = apollo_local_store
|
|
30
|
+
return { success: false, error: :no_store } unless store
|
|
31
|
+
|
|
32
|
+
partner_id = resolve_partner_id
|
|
33
|
+
model = attachment_store.get(partner_id) if partner_id
|
|
34
|
+
comm_patterns = read_communication_patterns(store, partner_id)
|
|
35
|
+
arc_state = read_relationship_arc(store, partner_id)
|
|
36
|
+
health = compute_relationship_health(model, comm_patterns, arc_state)
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
bonds_reflected: attachment_store.all_models.size,
|
|
40
|
+
partner_bond: if model
|
|
41
|
+
{
|
|
42
|
+
stage: model.bond_stage,
|
|
43
|
+
strength: model.attachment_strength,
|
|
44
|
+
style: model.attachment_style,
|
|
45
|
+
health: health,
|
|
46
|
+
milestones_today: arc_state[:milestones_today] || [],
|
|
47
|
+
narrative: nil
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
}
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
{ success: false, error: e.message }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def attachment_stats(**)
|
|
56
|
+
partner_id = resolve_partner_id
|
|
57
|
+
partner_model = attachment_store.get(partner_id) if partner_id
|
|
58
|
+
{
|
|
59
|
+
bonds_tracked: attachment_store.all_models.size,
|
|
60
|
+
partner_bond: partner_model&.to_h
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def attachment_store
|
|
67
|
+
@attachment_store ||= Helpers::AttachmentStore.new
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def collect_agent_ids(tick_results, human_observations)
|
|
71
|
+
ids = Set.new
|
|
72
|
+
(tick_results.dig(:social_cognition, :reputation_updates) || []).each do |u|
|
|
73
|
+
ids << u[:agent_id].to_s if u[:agent_id]
|
|
74
|
+
end
|
|
75
|
+
human_observations.each { |o| ids << o[:agent_id].to_s if o[:agent_id] }
|
|
76
|
+
ids.to_a
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def extract_signals(agent_id, tick_results, human_observations)
|
|
80
|
+
reputation = (tick_results.dig(:social_cognition, :reputation_updates) || [])
|
|
81
|
+
.find { |u| u[:agent_id].to_s == agent_id }
|
|
82
|
+
prediction = tick_results.dig(:theory_of_mind, :prediction_accuracy) || {}
|
|
83
|
+
obs = human_observations.select { |o| o[:agent_id].to_s == agent_id }
|
|
84
|
+
|
|
85
|
+
direct_count = obs.count { |o| o[:direct_address] }
|
|
86
|
+
channels = obs.filter_map { |o| o[:channel] }.uniq
|
|
87
|
+
|
|
88
|
+
{
|
|
89
|
+
frequency_score: obs.size.clamp(0, 10) / 10.0,
|
|
90
|
+
reciprocity_score: (reputation&.dig(:composite) || 0.0).clamp(0.0, 1.0),
|
|
91
|
+
prediction_accuracy: (prediction[agent_id] || 0.0).clamp(0.0, 1.0),
|
|
92
|
+
direct_address_ratio: obs.empty? ? 0.0 : (direct_count.to_f / obs.size).clamp(0.0, 1.0),
|
|
93
|
+
channel_consistency: channels.size <= 1 ? 1.0 : (1.0 / channels.size).clamp(0.0, 1.0)
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def extract_style_signals(agent_id, human_observations)
|
|
98
|
+
obs = human_observations.select { |o| o[:agent_id].to_s == agent_id }
|
|
99
|
+
direct_count = obs.count { |o| o[:direct_address] }
|
|
100
|
+
{
|
|
101
|
+
frequency_variance: 0.0,
|
|
102
|
+
reciprocity_imbalance: 0.0,
|
|
103
|
+
frequency: obs.size.clamp(0, 10) / 10.0,
|
|
104
|
+
direct_address_ratio: obs.empty? ? 0.0 : direct_count.to_f / obs.size
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def resolve_partner_id
|
|
109
|
+
if defined?(Legion::Gaia::BondRegistry)
|
|
110
|
+
bond = Legion::Gaia::BondRegistry.all_bonds.find { |b| b[:role] == :partner }
|
|
111
|
+
return bond&.dig(:identity)&.to_s
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
strongest = attachment_store.all_models.max_by(&:attachment_strength)
|
|
115
|
+
strongest&.agent_id
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def apollo_local_store
|
|
119
|
+
return nil unless defined?(Legion::Apollo::Local) && Legion::Apollo::Local.started?
|
|
120
|
+
|
|
121
|
+
Legion::Apollo::Local
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def read_communication_patterns(store, partner_id)
|
|
125
|
+
return {} unless partner_id
|
|
126
|
+
|
|
127
|
+
result = store.query(text: 'communication_pattern',
|
|
128
|
+
tags: ['bond', 'communication_pattern', partner_id])
|
|
129
|
+
return {} unless result[:success] && result[:results]&.any?
|
|
130
|
+
|
|
131
|
+
deserialize(result[:results].first[:content]) || {}
|
|
132
|
+
rescue StandardError => _e
|
|
133
|
+
{}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def read_relationship_arc(store, partner_id)
|
|
137
|
+
return {} unless partner_id
|
|
138
|
+
|
|
139
|
+
result = store.query(text: 'relationship_arc',
|
|
140
|
+
tags: ['bond', 'relationship_arc', partner_id])
|
|
141
|
+
return {} unless result[:success] && result[:results]&.any?
|
|
142
|
+
|
|
143
|
+
deserialize(result[:results].first[:content]) || {}
|
|
144
|
+
rescue StandardError => _e
|
|
145
|
+
{}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def compute_relationship_health(model, comm_patterns, arc_state)
|
|
149
|
+
return 0.0 unless model
|
|
150
|
+
|
|
151
|
+
strength_component = model.attachment_strength * 0.4
|
|
152
|
+
reciprocity_component = (arc_state[:reciprocity_balance] || 0.5) * 0.3
|
|
153
|
+
consistency_component = (comm_patterns[:consistency] || 0.5) * 0.3
|
|
154
|
+
(strength_component + reciprocity_component + consistency_component).clamp(0.0, 1.0)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def deserialize(content)
|
|
158
|
+
parsed = defined?(Legion::JSON) ? Legion::JSON.parse(content) : ::JSON.parse(content, symbolize_names: true)
|
|
159
|
+
parsed.is_a?(Hash) ? parsed.transform_keys(&:to_sym) : {}
|
|
160
|
+
rescue StandardError => _e
|
|
161
|
+
{}
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'attachment/version'
|
|
4
|
+
require_relative 'attachment/helpers/constants'
|
|
5
|
+
require_relative 'attachment/helpers/attachment_model'
|
|
6
|
+
require_relative 'attachment/helpers/attachment_store'
|
|
7
|
+
require_relative 'attachment/runners/attachment'
|
|
8
|
+
require_relative 'attachment/client'
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/agentic/social/attachment/helpers/constants'
|
|
5
|
+
require 'legion/extensions/agentic/social/attachment/helpers/attachment_model'
|
|
6
|
+
|
|
7
|
+
RSpec.describe Legion::Extensions::Agentic::Social::Attachment::Helpers::AttachmentModel do
|
|
8
|
+
subject(:model) { described_class.new(agent_id: 'partner-1') }
|
|
9
|
+
|
|
10
|
+
describe '#initialize' do
|
|
11
|
+
it 'sets agent_id' do
|
|
12
|
+
expect(model.agent_id).to eq('partner-1')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'starts at initial stage' do
|
|
16
|
+
expect(model.bond_stage).to eq(:initial)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'starts with zero strength' do
|
|
20
|
+
expect(model.attachment_strength).to eq(0.0)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'starts with secure style' do
|
|
24
|
+
expect(model.attachment_style).to eq(:secure)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'starts with base separation tolerance' do
|
|
28
|
+
expect(model.separation_tolerance).to eq(3)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'starts with zero interaction count' do
|
|
32
|
+
expect(model.interaction_count).to eq(0)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe '#update_from_signals' do
|
|
37
|
+
let(:signals) do
|
|
38
|
+
{
|
|
39
|
+
frequency_score: 0.6,
|
|
40
|
+
reciprocity_score: 0.5,
|
|
41
|
+
prediction_accuracy: 0.7,
|
|
42
|
+
direct_address_ratio: 0.4,
|
|
43
|
+
channel_consistency: 0.8
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'computes attachment strength from weighted signals' do
|
|
48
|
+
model.update_from_signals(signals)
|
|
49
|
+
expected = (0.6 * 0.3) + (0.5 * 0.25) + (0.7 * 0.2) + (0.4 * 0.15) + (0.8 * 0.1)
|
|
50
|
+
expect(model.attachment_strength).to be_within(0.01).of(expected)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'increments interaction count' do
|
|
54
|
+
model.update_from_signals(signals)
|
|
55
|
+
expect(model.interaction_count).to eq(1)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'uses EMA for subsequent updates' do
|
|
59
|
+
model.update_from_signals(signals)
|
|
60
|
+
first_strength = model.attachment_strength
|
|
61
|
+
model.update_from_signals(signals.merge(frequency_score: 1.0))
|
|
62
|
+
expect(model.attachment_strength).not_to eq(first_strength)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe '#update_stage!' do
|
|
67
|
+
it 'transitions to forming at 10 interactions and strength > 0.3' do
|
|
68
|
+
model.instance_variable_set(:@interaction_count, 10)
|
|
69
|
+
model.instance_variable_set(:@attachment_strength, 0.35)
|
|
70
|
+
model.update_stage!
|
|
71
|
+
expect(model.bond_stage).to eq(:forming)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'transitions to established at 50 interactions and strength > 0.5' do
|
|
75
|
+
model.instance_variable_set(:@interaction_count, 50)
|
|
76
|
+
model.instance_variable_set(:@attachment_strength, 0.55)
|
|
77
|
+
model.instance_variable_set(:@bond_stage, :forming)
|
|
78
|
+
model.update_stage!
|
|
79
|
+
expect(model.bond_stage).to eq(:established)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'transitions to deep at 200 interactions and strength > 0.7' do
|
|
83
|
+
model.instance_variable_set(:@interaction_count, 200)
|
|
84
|
+
model.instance_variable_set(:@attachment_strength, 0.75)
|
|
85
|
+
model.instance_variable_set(:@bond_stage, :established)
|
|
86
|
+
model.update_stage!
|
|
87
|
+
expect(model.bond_stage).to eq(:deep)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'never regresses stages' do
|
|
91
|
+
model.instance_variable_set(:@bond_stage, :established)
|
|
92
|
+
model.instance_variable_set(:@interaction_count, 5)
|
|
93
|
+
model.instance_variable_set(:@attachment_strength, 0.1)
|
|
94
|
+
model.update_stage!
|
|
95
|
+
expect(model.bond_stage).to eq(:established)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'updates separation tolerance on stage change' do
|
|
99
|
+
model.instance_variable_set(:@interaction_count, 50)
|
|
100
|
+
model.instance_variable_set(:@attachment_strength, 0.55)
|
|
101
|
+
model.instance_variable_set(:@bond_stage, :forming)
|
|
102
|
+
model.update_stage!
|
|
103
|
+
expect(model.separation_tolerance).to eq(3 + 2)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe '#derive_style!' do
|
|
108
|
+
it 'defaults to secure' do
|
|
109
|
+
model.derive_style!(frequency_variance: 0.1, reciprocity_imbalance: 0.1,
|
|
110
|
+
frequency: 0.5, direct_address_ratio: 0.5)
|
|
111
|
+
expect(model.attachment_style).to eq(:secure)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'detects anxious from high variance and imbalance' do
|
|
115
|
+
model.derive_style!(frequency_variance: 0.5, reciprocity_imbalance: 0.4,
|
|
116
|
+
frequency: 0.5, direct_address_ratio: 0.5)
|
|
117
|
+
expect(model.attachment_style).to eq(:anxious)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'detects avoidant from low frequency and low direct address' do
|
|
121
|
+
model.derive_style!(frequency_variance: 0.1, reciprocity_imbalance: 0.1,
|
|
122
|
+
frequency: 0.1, direct_address_ratio: 0.1)
|
|
123
|
+
expect(model.attachment_style).to eq(:avoidant)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
describe '#to_h' do
|
|
128
|
+
it 'returns a complete hash' do
|
|
129
|
+
h = model.to_h
|
|
130
|
+
expect(h).to include(:agent_id, :attachment_strength, :attachment_style,
|
|
131
|
+
:bond_stage, :separation_tolerance, :interaction_count)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
describe 'serialization' do
|
|
136
|
+
it 'round-trips through from_h' do
|
|
137
|
+
model.instance_variable_set(:@attachment_strength, 0.65)
|
|
138
|
+
model.instance_variable_set(:@bond_stage, :established)
|
|
139
|
+
model.instance_variable_set(:@interaction_count, 55)
|
|
140
|
+
restored = described_class.from_h(model.to_h)
|
|
141
|
+
expect(restored.to_h).to eq(model.to_h)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/agentic/social/attachment/helpers/constants'
|
|
5
|
+
require 'legion/extensions/agentic/social/attachment/helpers/attachment_model'
|
|
6
|
+
require 'legion/extensions/agentic/social/attachment/helpers/attachment_store'
|
|
7
|
+
|
|
8
|
+
RSpec.describe Legion::Extensions::Agentic::Social::Attachment::Helpers::AttachmentStore do
|
|
9
|
+
subject(:store) { described_class.new }
|
|
10
|
+
|
|
11
|
+
describe '#get' do
|
|
12
|
+
it 'returns nil for unknown agent' do
|
|
13
|
+
expect(store.get('unknown')).to be_nil
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe '#get_or_create' do
|
|
18
|
+
it 'creates a new model for unknown agent' do
|
|
19
|
+
model = store.get_or_create('agent-1')
|
|
20
|
+
expect(model).to be_a(Legion::Extensions::Agentic::Social::Attachment::Helpers::AttachmentModel)
|
|
21
|
+
expect(model.agent_id).to eq('agent-1')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'returns existing model on second call' do
|
|
25
|
+
first = store.get_or_create('agent-1')
|
|
26
|
+
second = store.get_or_create('agent-1')
|
|
27
|
+
expect(first).to equal(second)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe '#dirty?' do
|
|
32
|
+
it 'starts clean' do
|
|
33
|
+
expect(store).not_to be_dirty
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'becomes dirty after get_or_create' do
|
|
37
|
+
store.get_or_create('agent-1')
|
|
38
|
+
expect(store).to be_dirty
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '#mark_clean!' do
|
|
43
|
+
it 'clears dirty flag' do
|
|
44
|
+
store.get_or_create('agent-1')
|
|
45
|
+
store.mark_clean!
|
|
46
|
+
expect(store).not_to be_dirty
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'returns self for chaining' do
|
|
50
|
+
expect(store.mark_clean!).to eq(store)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe '#to_apollo_entries' do
|
|
55
|
+
before { store.get_or_create('partner-1') }
|
|
56
|
+
|
|
57
|
+
it 'returns an array of entry hashes' do
|
|
58
|
+
entries = store.to_apollo_entries
|
|
59
|
+
expect(entries).to be_an(Array)
|
|
60
|
+
expect(entries.size).to eq(1)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'includes content and tags' do
|
|
64
|
+
entry = store.to_apollo_entries.first
|
|
65
|
+
expect(entry).to have_key(:content)
|
|
66
|
+
expect(entry).to have_key(:tags)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'tags with bond, attachment, and agent_id' do
|
|
70
|
+
entry = store.to_apollo_entries.first
|
|
71
|
+
expect(entry[:tags]).to include('bond', 'attachment', 'partner-1')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'adds partner tag when BondRegistry says partner' do
|
|
75
|
+
bond_registry = class_double('Legion::Gaia::BondRegistry')
|
|
76
|
+
stub_const('Legion::Gaia::BondRegistry', bond_registry)
|
|
77
|
+
allow(bond_registry).to receive(:partner?).with('partner-1').and_return(true)
|
|
78
|
+
entry = store.to_apollo_entries.first
|
|
79
|
+
expect(entry[:tags]).to include('partner')
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
describe '#from_apollo' do
|
|
84
|
+
let(:mock_store) { double('apollo_local') }
|
|
85
|
+
|
|
86
|
+
it 'restores models from query results' do
|
|
87
|
+
model = Legion::Extensions::Agentic::Social::Attachment::Helpers::AttachmentModel.new(agent_id: 'p1')
|
|
88
|
+
model.instance_variable_set(:@attachment_strength, 0.6)
|
|
89
|
+
model.instance_variable_set(:@bond_stage, :established)
|
|
90
|
+
content = Legion::JSON.dump(model.to_h) if defined?(Legion::JSON)
|
|
91
|
+
content ||= JSON.dump(model.to_h)
|
|
92
|
+
|
|
93
|
+
allow(mock_store).to receive(:query)
|
|
94
|
+
.with(text: 'bond attachment', tags: %w[bond attachment])
|
|
95
|
+
.and_return({ success: true, results: [{ content: content, tags: %w[bond attachment p1] }] })
|
|
96
|
+
|
|
97
|
+
result = store.from_apollo(store: mock_store)
|
|
98
|
+
expect(result).to be true
|
|
99
|
+
expect(store.get('p1')).not_to be_nil
|
|
100
|
+
expect(store.get('p1').attachment_strength).to eq(0.6)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'returns false on empty results' do
|
|
104
|
+
allow(mock_store).to receive(:query).and_return({ success: true, results: [] })
|
|
105
|
+
expect(store.from_apollo(store: mock_store)).to be false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'returns false on error' do
|
|
109
|
+
allow(mock_store).to receive(:query).and_raise(StandardError, 'db error')
|
|
110
|
+
expect(store.from_apollo(store: mock_store)).to be false
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
describe '#all_models' do
|
|
115
|
+
it 'returns all tracked models' do
|
|
116
|
+
store.get_or_create('a1')
|
|
117
|
+
store.get_or_create('a2')
|
|
118
|
+
expect(store.all_models.size).to eq(2)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/agentic/social/attachment/helpers/constants'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::Agentic::Social::Attachment::Helpers::Constants do
|
|
7
|
+
describe 'strength weights' do
|
|
8
|
+
it 'sums to 1.0' do
|
|
9
|
+
sum = described_class::FREQUENCY_WEIGHT +
|
|
10
|
+
described_class::RECIPROCITY_WEIGHT +
|
|
11
|
+
described_class::PREDICTION_ACCURACY_WEIGHT +
|
|
12
|
+
described_class::DIRECT_ADDRESS_WEIGHT +
|
|
13
|
+
described_class::CHANNEL_CONSISTENCY_WEIGHT
|
|
14
|
+
expect(sum).to eq(1.0)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe 'BOND_STAGES' do
|
|
19
|
+
it 'defines 4 stages in order' do
|
|
20
|
+
expect(described_class::BOND_STAGES).to eq(%i[initial forming established deep])
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe 'ATTACHMENT_STYLES' do
|
|
25
|
+
it 'defines 3 styles' do
|
|
26
|
+
expect(described_class::ATTACHMENT_STYLES).to eq(%i[secure anxious avoidant])
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe 'stage thresholds' do
|
|
31
|
+
it 'has forming thresholds' do
|
|
32
|
+
expect(described_class::STAGE_THRESHOLDS[:forming]).to eq({ interactions: 10, strength: 0.3 })
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'has established thresholds' do
|
|
36
|
+
expect(described_class::STAGE_THRESHOLDS[:established]).to eq({ interactions: 50, strength: 0.5 })
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'has deep thresholds' do
|
|
40
|
+
expect(described_class::STAGE_THRESHOLDS[:deep]).to eq({ interactions: 200, strength: 0.7 })
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe 'SEPARATION_TOLERANCE' do
|
|
45
|
+
it 'starts at 3' do
|
|
46
|
+
expect(described_class::BASE_SEPARATION_TOLERANCE).to eq(3)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'grows with stage' do
|
|
50
|
+
expect(described_class::SEPARATION_TOLERANCE_GROWTH[:deep]).to be > described_class::SEPARATION_TOLERANCE_GROWTH[:initial]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'legion/extensions/agentic/social/attachment/helpers/constants'
|
|
5
|
+
require 'legion/extensions/agentic/social/attachment/helpers/attachment_model'
|
|
6
|
+
require 'legion/extensions/agentic/social/attachment/helpers/attachment_store'
|
|
7
|
+
require 'legion/extensions/agentic/social/attachment/runners/attachment'
|
|
8
|
+
|
|
9
|
+
RSpec.describe Legion::Extensions::Agentic::Social::Attachment::Runners::Attachment do
|
|
10
|
+
let(:host) { Object.new.extend(described_class) }
|
|
11
|
+
|
|
12
|
+
before do
|
|
13
|
+
# Reset memoized store between specs
|
|
14
|
+
host.instance_variable_set(:@attachment_store, nil)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe '#update_attachment' do
|
|
18
|
+
let(:tick_results) do
|
|
19
|
+
{
|
|
20
|
+
social_cognition: {
|
|
21
|
+
reputation_updates: [
|
|
22
|
+
{ agent_id: 'partner-1', composite: 0.6 }
|
|
23
|
+
],
|
|
24
|
+
reciprocity_ledger_size: 10
|
|
25
|
+
},
|
|
26
|
+
theory_of_mind: {
|
|
27
|
+
prediction_accuracy: { 'partner-1' => 0.7 }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
let(:human_observations) do
|
|
33
|
+
[
|
|
34
|
+
{ agent_id: 'partner-1', direct_address: true, channel: 'teams' },
|
|
35
|
+
{ agent_id: 'partner-1', direct_address: false, channel: 'teams' },
|
|
36
|
+
{ agent_id: 'partner-1', direct_address: true, channel: 'cli' }
|
|
37
|
+
]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'returns a result hash' do
|
|
41
|
+
result = host.update_attachment(tick_results: tick_results, human_observations: human_observations)
|
|
42
|
+
expect(result).to be_a(Hash)
|
|
43
|
+
expect(result).to have_key(:agents_updated)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'creates a model for observed agents' do
|
|
47
|
+
host.update_attachment(tick_results: tick_results, human_observations: human_observations)
|
|
48
|
+
store = host.send(:attachment_store)
|
|
49
|
+
expect(store.get('partner-1')).not_to be_nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'updates attachment strength' do
|
|
53
|
+
host.update_attachment(tick_results: tick_results, human_observations: human_observations)
|
|
54
|
+
model = host.send(:attachment_store).get('partner-1')
|
|
55
|
+
expect(model.attachment_strength).to be > 0.0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'marks store dirty' do
|
|
59
|
+
host.update_attachment(tick_results: tick_results, human_observations: human_observations)
|
|
60
|
+
expect(host.send(:attachment_store)).to be_dirty
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'handles empty tick results' do
|
|
64
|
+
result = host.update_attachment(tick_results: {}, human_observations: [])
|
|
65
|
+
expect(result[:agents_updated]).to eq(0)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
describe '#reflect_on_bonds' do
|
|
70
|
+
let(:mock_store) { double('apollo_local') }
|
|
71
|
+
|
|
72
|
+
before do
|
|
73
|
+
allow(host).to receive(:apollo_local_store).and_return(mock_store)
|
|
74
|
+
allow(mock_store).to receive(:query).and_return({ success: true, results: [] })
|
|
75
|
+
|
|
76
|
+
store = host.send(:attachment_store)
|
|
77
|
+
model = store.get_or_create('partner-1')
|
|
78
|
+
model.update_from_signals(frequency_score: 0.6, reciprocity_score: 0.5,
|
|
79
|
+
prediction_accuracy: 0.7, direct_address_ratio: 0.4,
|
|
80
|
+
channel_consistency: 0.8)
|
|
81
|
+
model.instance_variable_set(:@interaction_count, 55)
|
|
82
|
+
model.instance_variable_set(:@bond_stage, :established)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'returns bond reflection result' do
|
|
86
|
+
result = host.reflect_on_bonds(tick_results: {}, bond_summary: {})
|
|
87
|
+
expect(result).to have_key(:bonds_reflected)
|
|
88
|
+
expect(result).to have_key(:partner_bond)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'includes partner bond state' do
|
|
92
|
+
result = host.reflect_on_bonds(tick_results: {}, bond_summary: {})
|
|
93
|
+
bond = result[:partner_bond]
|
|
94
|
+
expect(bond[:stage]).to eq(:established)
|
|
95
|
+
expect(bond[:strength]).to be > 0.0
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'reads communication patterns from Apollo Local' do
|
|
99
|
+
host.reflect_on_bonds(tick_results: {}, bond_summary: {})
|
|
100
|
+
expect(mock_store).to have_received(:query)
|
|
101
|
+
.with(hash_including(tags: array_including('bond', 'communication_pattern')))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'reads relationship arc from Apollo Local' do
|
|
105
|
+
host.reflect_on_bonds(tick_results: {}, bond_summary: {})
|
|
106
|
+
expect(mock_store).to have_received(:query)
|
|
107
|
+
.with(hash_including(tags: array_including('bond', 'relationship_arc')))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'returns error when no store available' do
|
|
111
|
+
allow(host).to receive(:apollo_local_store).and_return(nil)
|
|
112
|
+
result = host.reflect_on_bonds(tick_results: {}, bond_summary: {})
|
|
113
|
+
expect(result[:success]).to be false
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'computes relationship health' do
|
|
117
|
+
result = host.reflect_on_bonds(tick_results: {}, bond_summary: {})
|
|
118
|
+
expect(result[:partner_bond][:health]).to be_a(Float)
|
|
119
|
+
expect(result[:partner_bond][:health]).to be_between(0.0, 1.0)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
describe '#attachment_stats' do
|
|
124
|
+
it 'returns stats hash' do
|
|
125
|
+
result = host.attachment_stats
|
|
126
|
+
expect(result).to have_key(:bonds_tracked)
|
|
127
|
+
expect(result).to have_key(:partner_bond)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-agentic-social
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -125,44 +125,44 @@ dependencies:
|
|
|
125
125
|
name: rubocop
|
|
126
126
|
requirement: !ruby/object:Gem::Requirement
|
|
127
127
|
requirements:
|
|
128
|
-
- - "
|
|
128
|
+
- - ">="
|
|
129
129
|
- !ruby/object:Gem::Version
|
|
130
|
-
version: '
|
|
130
|
+
version: '0'
|
|
131
131
|
type: :development
|
|
132
132
|
prerelease: false
|
|
133
133
|
version_requirements: !ruby/object:Gem::Requirement
|
|
134
134
|
requirements:
|
|
135
|
-
- - "
|
|
135
|
+
- - ">="
|
|
136
136
|
- !ruby/object:Gem::Version
|
|
137
|
-
version: '
|
|
137
|
+
version: '0'
|
|
138
138
|
- !ruby/object:Gem::Dependency
|
|
139
139
|
name: rubocop-legion
|
|
140
140
|
requirement: !ruby/object:Gem::Requirement
|
|
141
141
|
requirements:
|
|
142
|
-
- - "
|
|
142
|
+
- - ">="
|
|
143
143
|
- !ruby/object:Gem::Version
|
|
144
|
-
version: '0
|
|
144
|
+
version: '0'
|
|
145
145
|
type: :development
|
|
146
146
|
prerelease: false
|
|
147
147
|
version_requirements: !ruby/object:Gem::Requirement
|
|
148
148
|
requirements:
|
|
149
|
-
- - "
|
|
149
|
+
- - ">="
|
|
150
150
|
- !ruby/object:Gem::Version
|
|
151
|
-
version: '0
|
|
151
|
+
version: '0'
|
|
152
152
|
- !ruby/object:Gem::Dependency
|
|
153
153
|
name: rubocop-rspec
|
|
154
154
|
requirement: !ruby/object:Gem::Requirement
|
|
155
155
|
requirements:
|
|
156
|
-
- - "
|
|
156
|
+
- - ">="
|
|
157
157
|
- !ruby/object:Gem::Version
|
|
158
|
-
version: '
|
|
158
|
+
version: '0'
|
|
159
159
|
type: :development
|
|
160
160
|
prerelease: false
|
|
161
161
|
version_requirements: !ruby/object:Gem::Requirement
|
|
162
162
|
requirements:
|
|
163
|
-
- - "
|
|
163
|
+
- - ">="
|
|
164
164
|
- !ruby/object:Gem::Version
|
|
165
|
-
version: '
|
|
165
|
+
version: '0'
|
|
166
166
|
description: 'LEX agentic social domain: empathy, social cognition, theory of mind'
|
|
167
167
|
email:
|
|
168
168
|
- matthewdiverson@gmail.com
|
|
@@ -183,6 +183,13 @@ files:
|
|
|
183
183
|
- lib/legion/extensions/agentic/social/apprenticeship/helpers/apprenticeship_model.rb
|
|
184
184
|
- lib/legion/extensions/agentic/social/apprenticeship/runners/cognitive_apprenticeship.rb
|
|
185
185
|
- lib/legion/extensions/agentic/social/apprenticeship/version.rb
|
|
186
|
+
- lib/legion/extensions/agentic/social/attachment.rb
|
|
187
|
+
- lib/legion/extensions/agentic/social/attachment/client.rb
|
|
188
|
+
- lib/legion/extensions/agentic/social/attachment/helpers/attachment_model.rb
|
|
189
|
+
- lib/legion/extensions/agentic/social/attachment/helpers/attachment_store.rb
|
|
190
|
+
- lib/legion/extensions/agentic/social/attachment/helpers/constants.rb
|
|
191
|
+
- lib/legion/extensions/agentic/social/attachment/runners/attachment.rb
|
|
192
|
+
- lib/legion/extensions/agentic/social/attachment/version.rb
|
|
186
193
|
- lib/legion/extensions/agentic/social/conflict.rb
|
|
187
194
|
- lib/legion/extensions/agentic/social/conflict/actors/stale_check.rb
|
|
188
195
|
- lib/legion/extensions/agentic/social/conflict/client.rb
|
|
@@ -317,6 +324,10 @@ files:
|
|
|
317
324
|
- spec/legion/extensions/agentic/social/apprenticeship/helpers/apprenticeship_model_spec.rb
|
|
318
325
|
- spec/legion/extensions/agentic/social/apprenticeship/helpers/apprenticeship_spec.rb
|
|
319
326
|
- spec/legion/extensions/agentic/social/apprenticeship/runners/cognitive_apprenticeship_spec.rb
|
|
327
|
+
- spec/legion/extensions/agentic/social/attachment/helpers/attachment_model_spec.rb
|
|
328
|
+
- spec/legion/extensions/agentic/social/attachment/helpers/attachment_store_spec.rb
|
|
329
|
+
- spec/legion/extensions/agentic/social/attachment/helpers/constants_spec.rb
|
|
330
|
+
- spec/legion/extensions/agentic/social/attachment/runners/attachment_spec.rb
|
|
320
331
|
- spec/legion/extensions/agentic/social/conflict/actors/stale_check_spec.rb
|
|
321
332
|
- spec/legion/extensions/agentic/social/conflict/client_spec.rb
|
|
322
333
|
- spec/legion/extensions/agentic/social/conflict/helpers/conflict_log_spec.rb
|