lex-dissonance 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 +7 -0
- data/Gemfile +10 -0
- data/lex-dissonance.gemspec +29 -0
- data/lib/legion/extensions/dissonance/client.rb +28 -0
- data/lib/legion/extensions/dissonance/helpers/belief.rb +42 -0
- data/lib/legion/extensions/dissonance/helpers/constants.rb +23 -0
- data/lib/legion/extensions/dissonance/helpers/dissonance_event.rb +48 -0
- data/lib/legion/extensions/dissonance/helpers/dissonance_model.rb +155 -0
- data/lib/legion/extensions/dissonance/runners/dissonance.rb +159 -0
- data/lib/legion/extensions/dissonance/version.rb +9 -0
- data/lib/legion/extensions/dissonance.rb +16 -0
- data/spec/legion/extensions/dissonance/client_spec.rb +51 -0
- data/spec/legion/extensions/dissonance/helpers/belief_spec.rb +103 -0
- data/spec/legion/extensions/dissonance/helpers/constants_spec.rb +60 -0
- data/spec/legion/extensions/dissonance/helpers/dissonance_event_spec.rb +113 -0
- data/spec/legion/extensions/dissonance/helpers/dissonance_model_spec.rb +252 -0
- data/spec/legion/extensions/dissonance/runners/dissonance_spec.rb +323 -0
- data/spec/spec_helper.rb +28 -0
- metadata +78 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 93ff7acbd3ccecfcaf1bcdf7ea395eb09507a278423c1a5fb5f0349e7ef59c43
|
|
4
|
+
data.tar.gz: ca429797ec391379c0eb88c0ecd07c429686b0f4166433e3716ad05e9db25634
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3619a6c4544ae2ac31410c59ca0e2b6cbec8ea687283718e83ccbf3b1d57bb7c20ccfe115e925f2876d29025d573971e7429ad1c45535777308477b8e699e640
|
|
7
|
+
data.tar.gz: 3acc77063b498cda63078d5261eba333c6d14affc1c618b4f85f8e5e5aa319031a9b01ea57cd9b89cdf9fa203805ad35d92f992fc5eacdbbab8b687f8b9f3a04
|
data/Gemfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/dissonance/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-dissonance'
|
|
7
|
+
spec.version = Legion::Extensions::Dissonance::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Dissonance'
|
|
12
|
+
spec.description = 'Cognitive dissonance modeling — contradiction detection, belief tracking, and resolution strategies for brain-modeled agentic AI'
|
|
13
|
+
spec.homepage = 'https://github.com/LegionIO/lex-dissonance'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 3.4'
|
|
16
|
+
|
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-dissonance'
|
|
19
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-dissonance'
|
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-dissonance'
|
|
21
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-dissonance/issues'
|
|
22
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
23
|
+
|
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
25
|
+
Dir.glob('{lib,spec}/**/*') + %w[lex-dissonance.gemspec Gemfile]
|
|
26
|
+
end
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
spec.add_development_dependency 'legion-gaia'
|
|
29
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/dissonance/helpers/constants'
|
|
4
|
+
require 'legion/extensions/dissonance/helpers/belief'
|
|
5
|
+
require 'legion/extensions/dissonance/helpers/dissonance_event'
|
|
6
|
+
require 'legion/extensions/dissonance/helpers/dissonance_model'
|
|
7
|
+
require 'legion/extensions/dissonance/runners/dissonance'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module Dissonance
|
|
12
|
+
class Client
|
|
13
|
+
include Runners::Dissonance
|
|
14
|
+
|
|
15
|
+
attr_reader :model
|
|
16
|
+
|
|
17
|
+
def initialize(model: nil, **)
|
|
18
|
+
@model = model || Helpers::DissonanceModel.new
|
|
19
|
+
@dissonance_model = @model
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_accessor :dissonance_model
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Dissonance
|
|
8
|
+
module Helpers
|
|
9
|
+
class Belief
|
|
10
|
+
attr_reader :id, :domain, :content, :confidence, :importance, :created_at
|
|
11
|
+
|
|
12
|
+
def initialize(domain:, content:, confidence: 0.7, importance: :moderate)
|
|
13
|
+
@id = SecureRandom.uuid
|
|
14
|
+
@domain = domain
|
|
15
|
+
@content = content
|
|
16
|
+
@confidence = confidence.clamp(0.0, 1.0)
|
|
17
|
+
@importance = importance
|
|
18
|
+
@created_at = Time.now.utc
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def contradicts?(other)
|
|
22
|
+
return false if id == other.id
|
|
23
|
+
return false unless domain == other.domain
|
|
24
|
+
|
|
25
|
+
content.strip.downcase != other.content.strip.downcase
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_h
|
|
29
|
+
{
|
|
30
|
+
id: id,
|
|
31
|
+
domain: domain,
|
|
32
|
+
content: content,
|
|
33
|
+
confidence: confidence,
|
|
34
|
+
importance: importance,
|
|
35
|
+
created_at: created_at
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Dissonance
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
DISSONANCE_THRESHOLD = 0.4
|
|
9
|
+
MAX_BELIEFS = 200
|
|
10
|
+
MAX_DISSONANCE_EVENTS = 100
|
|
11
|
+
DECAY_RATE = 0.03
|
|
12
|
+
RESOLUTION_RELIEF = 0.3
|
|
13
|
+
RATIONALIZATION_FACTOR = 0.5
|
|
14
|
+
IMPORTANCE_WEIGHTS = { core: 1.0, significant: 0.7, moderate: 0.5, peripheral: 0.25 }.freeze
|
|
15
|
+
RESOLUTION_STRATEGIES = %i[belief_revision rationalization avoidance].freeze
|
|
16
|
+
STRESS_CEILING = 1.0
|
|
17
|
+
STRESS_FLOOR = 0.0
|
|
18
|
+
CONTRADICTION_TYPES = %i[direct inverse conditional temporal].freeze
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Dissonance
|
|
8
|
+
module Helpers
|
|
9
|
+
class DissonanceEvent
|
|
10
|
+
attr_reader :id, :belief_a_id, :belief_b_id, :domain, :magnitude,
|
|
11
|
+
:contradiction_type, :resolved, :resolution_strategy, :timestamp
|
|
12
|
+
|
|
13
|
+
def initialize(belief_a_id:, belief_b_id:, domain:, magnitude:, contradiction_type: :direct)
|
|
14
|
+
@id = SecureRandom.uuid
|
|
15
|
+
@belief_a_id = belief_a_id
|
|
16
|
+
@belief_b_id = belief_b_id
|
|
17
|
+
@domain = domain
|
|
18
|
+
@magnitude = magnitude.clamp(0.0, 1.0)
|
|
19
|
+
@contradiction_type = contradiction_type
|
|
20
|
+
@resolved = false
|
|
21
|
+
@resolution_strategy = nil
|
|
22
|
+
@timestamp = Time.now.utc
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def resolve!(strategy)
|
|
26
|
+
@resolved = true
|
|
27
|
+
@resolution_strategy = strategy
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_h
|
|
32
|
+
{
|
|
33
|
+
id: id,
|
|
34
|
+
belief_a_id: belief_a_id,
|
|
35
|
+
belief_b_id: belief_b_id,
|
|
36
|
+
domain: domain,
|
|
37
|
+
magnitude: magnitude,
|
|
38
|
+
contradiction_type: contradiction_type,
|
|
39
|
+
resolved: resolved,
|
|
40
|
+
resolution_strategy: resolution_strategy,
|
|
41
|
+
timestamp: timestamp
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Dissonance
|
|
6
|
+
module Helpers
|
|
7
|
+
class DissonanceModel
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :beliefs, :events, :stress
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@beliefs = {}
|
|
14
|
+
@events = {}
|
|
15
|
+
@stress = STRESS_FLOOR
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def add_belief(domain:, content:, confidence: 0.7, importance: :moderate)
|
|
19
|
+
prune_beliefs if @beliefs.size >= MAX_BELIEFS
|
|
20
|
+
|
|
21
|
+
belief = Belief.new(domain: domain, content: content,
|
|
22
|
+
confidence: confidence, importance: importance)
|
|
23
|
+
@beliefs[belief.id] = belief
|
|
24
|
+
|
|
25
|
+
new_events = detect_contradictions_for(belief)
|
|
26
|
+
new_events.each { |ev| @events[ev.id] = ev }
|
|
27
|
+
|
|
28
|
+
{ belief: belief, new_dissonance_events: new_events }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def detect_contradictions
|
|
32
|
+
new_events = []
|
|
33
|
+
belief_list = @beliefs.values
|
|
34
|
+
|
|
35
|
+
belief_list.each_with_index do |bel_a, idx|
|
|
36
|
+
belief_list[(idx + 1)..].each do |bel_b|
|
|
37
|
+
next unless bel_a.contradicts?(bel_b)
|
|
38
|
+
next if contradiction_tracked?(bel_a.id, bel_b.id)
|
|
39
|
+
|
|
40
|
+
ev = build_event(bel_a, bel_b)
|
|
41
|
+
@events[ev.id] = ev
|
|
42
|
+
new_events << ev
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
new_events
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def resolve(event_id, strategy:)
|
|
50
|
+
event = @events[event_id]
|
|
51
|
+
return nil unless event
|
|
52
|
+
return nil if event.resolved
|
|
53
|
+
return nil unless RESOLUTION_STRATEGIES.include?(strategy)
|
|
54
|
+
|
|
55
|
+
event.resolve!(strategy)
|
|
56
|
+
relief = compute_relief(strategy)
|
|
57
|
+
@stress = (@stress - relief).clamp(STRESS_FLOOR, STRESS_CEILING)
|
|
58
|
+
event
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def stress_level
|
|
62
|
+
@stress
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def domain_stress(domain)
|
|
66
|
+
unresolved = unresolved_events.select { |ev| ev.domain == domain }
|
|
67
|
+
return STRESS_FLOOR if unresolved.empty?
|
|
68
|
+
|
|
69
|
+
raw = unresolved.sum(&:magnitude) / MAX_DISSONANCE_EVENTS.to_f
|
|
70
|
+
raw.clamp(STRESS_FLOOR, STRESS_CEILING)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def unresolved_events
|
|
74
|
+
@events.values.reject(&:resolved)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def decay
|
|
78
|
+
unresolved = unresolved_events
|
|
79
|
+
if unresolved.any?
|
|
80
|
+
increment = DECAY_RATE * unresolved.size
|
|
81
|
+
@stress = (@stress + increment).clamp(STRESS_FLOOR, STRESS_CEILING)
|
|
82
|
+
else
|
|
83
|
+
@stress = (@stress - DECAY_RATE).clamp(STRESS_FLOOR, STRESS_CEILING)
|
|
84
|
+
end
|
|
85
|
+
@stress
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def to_h
|
|
89
|
+
{
|
|
90
|
+
beliefs: @beliefs.values.map(&:to_h),
|
|
91
|
+
events: @events.values.map(&:to_h),
|
|
92
|
+
stress: @stress,
|
|
93
|
+
unresolved_count: unresolved_events.size,
|
|
94
|
+
total_beliefs: @beliefs.size,
|
|
95
|
+
total_events: @events.size
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def detect_contradictions_for(new_belief)
|
|
102
|
+
new_events = []
|
|
103
|
+
|
|
104
|
+
@beliefs.each_value do |existing|
|
|
105
|
+
next if existing.id == new_belief.id
|
|
106
|
+
next unless new_belief.contradicts?(existing)
|
|
107
|
+
next if contradiction_tracked?(new_belief.id, existing.id)
|
|
108
|
+
|
|
109
|
+
ev = build_event(new_belief, existing)
|
|
110
|
+
new_events << ev
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
new_events
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def build_event(bel_a, bel_b)
|
|
117
|
+
magnitude = compute_magnitude(bel_a, bel_b)
|
|
118
|
+
DissonanceEvent.new(
|
|
119
|
+
belief_a_id: bel_a.id,
|
|
120
|
+
belief_b_id: bel_b.id,
|
|
121
|
+
domain: bel_a.domain,
|
|
122
|
+
magnitude: magnitude,
|
|
123
|
+
contradiction_type: :direct
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def compute_magnitude(bel_a, bel_b)
|
|
128
|
+
weight_a = IMPORTANCE_WEIGHTS.fetch(bel_a.importance, 0.5)
|
|
129
|
+
weight_b = IMPORTANCE_WEIGHTS.fetch(bel_b.importance, 0.5)
|
|
130
|
+
avg_weight = (weight_a + weight_b) / 2.0
|
|
131
|
+
avg_confidence = (bel_a.confidence + bel_b.confidence) / 2.0
|
|
132
|
+
(avg_weight * avg_confidence).clamp(0.0, 1.0)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def compute_relief(strategy)
|
|
136
|
+
base = RESOLUTION_RELIEF
|
|
137
|
+
strategy == :rationalization ? base * RATIONALIZATION_FACTOR : base
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def contradiction_tracked?(id_a, id_b)
|
|
141
|
+
@events.values.any? do |ev|
|
|
142
|
+
(ev.belief_a_id == id_a && ev.belief_b_id == id_b) ||
|
|
143
|
+
(ev.belief_a_id == id_b && ev.belief_b_id == id_a)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def prune_beliefs
|
|
148
|
+
oldest = @beliefs.values.min_by(&:created_at)
|
|
149
|
+
@beliefs.delete(oldest.id) if oldest
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Dissonance
|
|
6
|
+
module Runners
|
|
7
|
+
module Dissonance
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def add_belief(domain:, content:, confidence: 0.7, importance: :moderate, **)
|
|
12
|
+
unless Helpers::Constants::IMPORTANCE_WEIGHTS.key?(importance)
|
|
13
|
+
return { success: false, error: :invalid_importance,
|
|
14
|
+
valid: Helpers::Constants::IMPORTANCE_WEIGHTS.keys }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
result = dissonance_model.add_belief(domain: domain, content: content,
|
|
18
|
+
confidence: confidence, importance: importance)
|
|
19
|
+
belief = result[:belief]
|
|
20
|
+
new_evs = result[:new_dissonance_events]
|
|
21
|
+
|
|
22
|
+
Legion::Logging.debug "[dissonance] add_belief: id=#{belief.id[0..7]} domain=#{domain} importance=#{importance} new_events=#{new_evs.size}"
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
success: true,
|
|
26
|
+
belief_id: belief.id,
|
|
27
|
+
domain: domain,
|
|
28
|
+
new_dissonance_events: new_evs.map(&:to_h),
|
|
29
|
+
dissonance_triggered: new_evs.any? { |ev| ev.magnitude >= Helpers::Constants::DISSONANCE_THRESHOLD }
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def update_dissonance(**)
|
|
34
|
+
dissonance_model.detect_contradictions
|
|
35
|
+
new_stress = dissonance_model.decay
|
|
36
|
+
unresolved = dissonance_model.unresolved_events
|
|
37
|
+
|
|
38
|
+
Legion::Logging.debug "[dissonance] update: stress=#{new_stress.round(3)} unresolved=#{unresolved.size}"
|
|
39
|
+
|
|
40
|
+
{
|
|
41
|
+
success: true,
|
|
42
|
+
stress: new_stress,
|
|
43
|
+
unresolved_count: unresolved.size,
|
|
44
|
+
above_threshold: unresolved.any? { |ev| ev.magnitude >= Helpers::Constants::DISSONANCE_THRESHOLD }
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def resolve_dissonance(event_id:, strategy: :belief_revision, **)
|
|
49
|
+
unless Helpers::Constants::RESOLUTION_STRATEGIES.include?(strategy)
|
|
50
|
+
return { success: false, error: :invalid_strategy,
|
|
51
|
+
valid: Helpers::Constants::RESOLUTION_STRATEGIES }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
event = dissonance_model.resolve(event_id, strategy: strategy)
|
|
55
|
+
if event
|
|
56
|
+
Legion::Logging.debug "[dissonance] resolved: id=#{event_id[0..7]} strategy=#{strategy}"
|
|
57
|
+
{ success: true, resolved: true, strategy: strategy, event: event.to_h }
|
|
58
|
+
else
|
|
59
|
+
Legion::Logging.debug "[dissonance] resolve failed: id=#{event_id[0..7]} not_found_or_already_resolved"
|
|
60
|
+
{ success: false, error: :not_found_or_already_resolved }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def dissonance_status(**)
|
|
65
|
+
model = dissonance_model
|
|
66
|
+
snapshot = model.to_h
|
|
67
|
+
|
|
68
|
+
Legion::Logging.debug "[dissonance] status: stress=#{snapshot[:stress].round(3)} " \
|
|
69
|
+
"beliefs=#{snapshot[:total_beliefs]} unresolved=#{snapshot[:unresolved_count]}"
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
success: true,
|
|
73
|
+
stress: snapshot[:stress],
|
|
74
|
+
total_beliefs: snapshot[:total_beliefs],
|
|
75
|
+
total_events: snapshot[:total_events],
|
|
76
|
+
unresolved_count: snapshot[:unresolved_count]
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def domain_dissonance(domain:, **)
|
|
81
|
+
stress = dissonance_model.domain_stress(domain)
|
|
82
|
+
unresolved = dissonance_model.unresolved_events.select { |ev| ev.domain == domain }
|
|
83
|
+
|
|
84
|
+
Legion::Logging.debug "[dissonance] domain_dissonance: domain=#{domain} stress=#{stress.round(3)} unresolved=#{unresolved.size}"
|
|
85
|
+
|
|
86
|
+
{
|
|
87
|
+
success: true,
|
|
88
|
+
domain: domain,
|
|
89
|
+
stress: stress,
|
|
90
|
+
unresolved_count: unresolved.size,
|
|
91
|
+
events: unresolved.map(&:to_h)
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def beliefs_for(domain:, **)
|
|
96
|
+
beliefs = dissonance_model.beliefs.values.select { |b| b.domain == domain }
|
|
97
|
+
|
|
98
|
+
Legion::Logging.debug "[dissonance] beliefs_for: domain=#{domain} count=#{beliefs.size}"
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
success: true,
|
|
102
|
+
domain: domain,
|
|
103
|
+
count: beliefs.size,
|
|
104
|
+
beliefs: beliefs.map(&:to_h)
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def unresolved(**)
|
|
109
|
+
events = dissonance_model.unresolved_events
|
|
110
|
+
|
|
111
|
+
Legion::Logging.debug "[dissonance] unresolved: count=#{events.size}"
|
|
112
|
+
|
|
113
|
+
{
|
|
114
|
+
success: true,
|
|
115
|
+
count: events.size,
|
|
116
|
+
events: events.map(&:to_h)
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def dissonance_stats(**)
|
|
121
|
+
model = dissonance_model
|
|
122
|
+
snapshot = model.to_h
|
|
123
|
+
|
|
124
|
+
domains = model.beliefs.values.map(&:domain).uniq
|
|
125
|
+
domain_stresses = domains.to_h { |d| [d, model.domain_stress(d)] }
|
|
126
|
+
|
|
127
|
+
unresolved = model.unresolved_events
|
|
128
|
+
resolved = model.events.values.select(&:resolved)
|
|
129
|
+
|
|
130
|
+
resolution_breakdown = Helpers::Constants::RESOLUTION_STRATEGIES.to_h do |s|
|
|
131
|
+
[s, resolved.count { |ev| ev.resolution_strategy == s }]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
Legion::Logging.debug "[dissonance] stats: beliefs=#{snapshot[:total_beliefs]} " \
|
|
135
|
+
"events=#{snapshot[:total_events]} stress=#{snapshot[:stress].round(3)}"
|
|
136
|
+
|
|
137
|
+
{
|
|
138
|
+
success: true,
|
|
139
|
+
stress: snapshot[:stress],
|
|
140
|
+
total_beliefs: snapshot[:total_beliefs],
|
|
141
|
+
total_events: snapshot[:total_events],
|
|
142
|
+
unresolved_count: unresolved.size,
|
|
143
|
+
resolved_count: resolved.size,
|
|
144
|
+
domain_stresses: domain_stresses,
|
|
145
|
+
resolution_breakdown: resolution_breakdown,
|
|
146
|
+
above_threshold: unresolved.any? { |ev| ev.magnitude >= Helpers::Constants::DISSONANCE_THRESHOLD }
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def dissonance_model
|
|
153
|
+
@dissonance_model ||= Helpers::DissonanceModel.new
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/dissonance/version'
|
|
4
|
+
require 'legion/extensions/dissonance/helpers/constants'
|
|
5
|
+
require 'legion/extensions/dissonance/helpers/belief'
|
|
6
|
+
require 'legion/extensions/dissonance/helpers/dissonance_event'
|
|
7
|
+
require 'legion/extensions/dissonance/helpers/dissonance_model'
|
|
8
|
+
require 'legion/extensions/dissonance/runners/dissonance'
|
|
9
|
+
|
|
10
|
+
module Legion
|
|
11
|
+
module Extensions
|
|
12
|
+
module Dissonance
|
|
13
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/dissonance/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Dissonance::Client do
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'creates a default DissonanceModel when none provided' do
|
|
8
|
+
client = described_class.new
|
|
9
|
+
expect(client.model).to be_a(Legion::Extensions::Dissonance::Helpers::DissonanceModel)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'accepts a custom model' do
|
|
13
|
+
custom_model = Legion::Extensions::Dissonance::Helpers::DissonanceModel.new
|
|
14
|
+
client = described_class.new(model: custom_model)
|
|
15
|
+
expect(client.model).to be(custom_model)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe 'responds to all runner methods' do
|
|
20
|
+
let(:client) { described_class.new }
|
|
21
|
+
|
|
22
|
+
it { expect(client).to respond_to(:add_belief) }
|
|
23
|
+
it { expect(client).to respond_to(:update_dissonance) }
|
|
24
|
+
it { expect(client).to respond_to(:resolve_dissonance) }
|
|
25
|
+
it { expect(client).to respond_to(:dissonance_status) }
|
|
26
|
+
it { expect(client).to respond_to(:domain_dissonance) }
|
|
27
|
+
it { expect(client).to respond_to(:beliefs_for) }
|
|
28
|
+
it { expect(client).to respond_to(:unresolved) }
|
|
29
|
+
it { expect(client).to respond_to(:dissonance_stats) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe 'state isolation between instances' do
|
|
33
|
+
it 'two clients do not share belief state' do
|
|
34
|
+
client1 = described_class.new
|
|
35
|
+
client2 = described_class.new
|
|
36
|
+
client1.add_belief(domain: 'x', content: 'some belief')
|
|
37
|
+
result = client2.beliefs_for(domain: 'x')
|
|
38
|
+
expect(result[:count]).to eq(0)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe 'custom model integration' do
|
|
43
|
+
it 'uses the beliefs already in a pre-populated model' do
|
|
44
|
+
model = Legion::Extensions::Dissonance::Helpers::DissonanceModel.new
|
|
45
|
+
model.add_belief(domain: 'shared', content: 'a shared belief')
|
|
46
|
+
client = described_class.new(model: model)
|
|
47
|
+
result = client.beliefs_for(domain: 'shared')
|
|
48
|
+
expect(result[:count]).to eq(1)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Dissonance::Helpers::Belief do
|
|
4
|
+
let(:belief) { described_class.new(domain: 'ethics', content: 'honesty is required', confidence: 0.9, importance: :core) }
|
|
5
|
+
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'assigns a uuid id' do
|
|
8
|
+
expect(belief.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'assigns domain' do
|
|
12
|
+
expect(belief.domain).to eq('ethics')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'assigns content' do
|
|
16
|
+
expect(belief.content).to eq('honesty is required')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'assigns confidence' do
|
|
20
|
+
expect(belief.confidence).to eq(0.9)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'assigns importance' do
|
|
24
|
+
expect(belief.importance).to eq(:core)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'assigns created_at as UTC Time' do
|
|
28
|
+
expect(belief.created_at).to be_a(Time)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'clamps confidence to 0-1 range (above)' do
|
|
32
|
+
b = described_class.new(domain: 'd', content: 'c', confidence: 1.5)
|
|
33
|
+
expect(b.confidence).to eq(1.0)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'clamps confidence to 0-1 range (below)' do
|
|
37
|
+
b = described_class.new(domain: 'd', content: 'c', confidence: -0.5)
|
|
38
|
+
expect(b.confidence).to eq(0.0)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'defaults confidence to 0.7' do
|
|
42
|
+
b = described_class.new(domain: 'd', content: 'c')
|
|
43
|
+
expect(b.confidence).to eq(0.7)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'defaults importance to :moderate' do
|
|
47
|
+
b = described_class.new(domain: 'd', content: 'c')
|
|
48
|
+
expect(b.importance).to eq(:moderate)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe '#contradicts?' do
|
|
53
|
+
let(:other_same) do
|
|
54
|
+
described_class.new(domain: 'ethics', content: 'honesty is required', confidence: 0.8, importance: :moderate)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
let(:other_different) do
|
|
58
|
+
described_class.new(domain: 'ethics', content: 'deception is acceptable', confidence: 0.6, importance: :moderate)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
let(:other_domain) do
|
|
62
|
+
described_class.new(domain: 'safety', content: 'honesty is required', confidence: 0.8, importance: :moderate)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'returns false for same content' do
|
|
66
|
+
expect(belief.contradicts?(other_same)).to be false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'returns true for different content in same domain' do
|
|
70
|
+
expect(belief.contradicts?(other_different)).to be true
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'returns false for different domain' do
|
|
74
|
+
expect(belief.contradicts?(other_domain)).to be false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'returns false when compared to itself' do
|
|
78
|
+
expect(belief.contradicts?(belief)).to be false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'is case-insensitive for content comparison' do
|
|
82
|
+
upper = described_class.new(domain: 'ethics', content: 'HONESTY IS REQUIRED', confidence: 0.8, importance: :moderate)
|
|
83
|
+
expect(belief.contradicts?(upper)).to be false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'ignores leading/trailing whitespace in content comparison' do
|
|
87
|
+
padded = described_class.new(domain: 'ethics', content: ' honesty is required ', confidence: 0.8, importance: :moderate)
|
|
88
|
+
expect(belief.contradicts?(padded)).to be false
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe '#to_h' do
|
|
93
|
+
it 'returns a hash with all fields' do
|
|
94
|
+
h = belief.to_h
|
|
95
|
+
expect(h[:id]).to eq(belief.id)
|
|
96
|
+
expect(h[:domain]).to eq('ethics')
|
|
97
|
+
expect(h[:content]).to eq('honesty is required')
|
|
98
|
+
expect(h[:confidence]).to eq(0.9)
|
|
99
|
+
expect(h[:importance]).to eq(:core)
|
|
100
|
+
expect(h[:created_at]).to be_a(Time)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|