lex-reflection 0.1.1
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 +15 -0
- data/LICENSE +21 -0
- data/README.md +86 -0
- data/lex-reflection.gemspec +29 -0
- data/lib/legion/extensions/reflection/client.rb +23 -0
- data/lib/legion/extensions/reflection/helpers/constants.rb +62 -0
- data/lib/legion/extensions/reflection/helpers/llm_enhancer.rb +162 -0
- data/lib/legion/extensions/reflection/helpers/monitors.rb +182 -0
- data/lib/legion/extensions/reflection/helpers/reflection.rb +50 -0
- data/lib/legion/extensions/reflection/helpers/reflection_store.rb +95 -0
- data/lib/legion/extensions/reflection/runners/reflection.rb +195 -0
- data/lib/legion/extensions/reflection/version.rb +9 -0
- data/lib/legion/extensions/reflection.rb +18 -0
- data/spec/legion/extensions/reflection/client_spec.rb +24 -0
- data/spec/legion/extensions/reflection/helpers/llm_enhancer_spec.rb +191 -0
- data/spec/legion/extensions/reflection/helpers/monitors_spec.rb +120 -0
- data/spec/legion/extensions/reflection/helpers/reflection_spec.rb +49 -0
- data/spec/legion/extensions/reflection/helpers/reflection_store_spec.rb +93 -0
- data/spec/legion/extensions/reflection/runners/reflection_spec.rb +204 -0
- data/spec/spec_helper.rb +20 -0
- metadata +80 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Reflection
|
|
6
|
+
module Helpers
|
|
7
|
+
class ReflectionStore
|
|
8
|
+
attr_reader :total_generated
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@reflections = {}
|
|
12
|
+
@total_generated = 0
|
|
13
|
+
@category_scores = Hash.new(1.0)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def store(reflection)
|
|
17
|
+
prune_oldest if @reflections.size >= Constants::MAX_REFLECTIONS
|
|
18
|
+
@reflections[reflection[:reflection_id]] = reflection
|
|
19
|
+
@total_generated += 1
|
|
20
|
+
reflection
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def get(reflection_id)
|
|
24
|
+
@reflections[reflection_id]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def recent(limit: 10)
|
|
28
|
+
@reflections.values
|
|
29
|
+
.sort_by { |r| r[:created_at] }
|
|
30
|
+
.last(limit)
|
|
31
|
+
.reverse
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def by_category(category)
|
|
35
|
+
@reflections.values.select { |r| r[:category] == category }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def by_severity(severity)
|
|
39
|
+
@reflections.values.select { |r| r[:severity] == severity }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def mark_acted_on(reflection_id)
|
|
43
|
+
reflection = @reflections[reflection_id]
|
|
44
|
+
return nil unless reflection
|
|
45
|
+
|
|
46
|
+
@reflections[reflection_id] = reflection.merge(acted_on: true)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def unacted
|
|
50
|
+
@reflections.values.reject { |r| r[:acted_on] }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def count
|
|
54
|
+
@reflections.size
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def severity_counts
|
|
58
|
+
counts = Hash.new(0)
|
|
59
|
+
@reflections.each_value { |r| counts[r[:severity]] += 1 }
|
|
60
|
+
counts
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def category_counts
|
|
64
|
+
counts = Hash.new(0)
|
|
65
|
+
@reflections.each_value { |r| counts[r[:category]] += 1 }
|
|
66
|
+
counts
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def update_category_score(category, score)
|
|
70
|
+
@category_scores[category] = score.clamp(0.0, 1.0)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def cognitive_health
|
|
74
|
+
total_weight = Constants::HEALTH_WEIGHTS.values.sum
|
|
75
|
+
weighted_sum = Constants::HEALTH_WEIGHTS.sum do |cat, weight|
|
|
76
|
+
@category_scores[cat] * weight
|
|
77
|
+
end
|
|
78
|
+
(weighted_sum / total_weight).round(3)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def category_score(category)
|
|
82
|
+
@category_scores[category]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def prune_oldest
|
|
88
|
+
oldest = @reflections.values.min_by { |r| r[:created_at] }
|
|
89
|
+
@reflections.delete(oldest[:reflection_id]) if oldest
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Reflection
|
|
6
|
+
module Runners
|
|
7
|
+
module Reflection
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def reflect(tick_results: {}, **)
|
|
12
|
+
@metric_history ||= []
|
|
13
|
+
@metric_history << tick_results
|
|
14
|
+
@metric_history = @metric_history.last(Helpers::Constants::METRIC_WINDOW_SIZE)
|
|
15
|
+
|
|
16
|
+
new_reflections = Helpers::Monitors.run_all(tick_results, @metric_history)
|
|
17
|
+
new_reflections.each { |r| reflection_store.store(r) }
|
|
18
|
+
|
|
19
|
+
update_category_scores(tick_results)
|
|
20
|
+
|
|
21
|
+
if Helpers::LlmEnhancer.available? && new_reflections.any?
|
|
22
|
+
health_scores = Helpers::Constants::CATEGORIES.to_h { |c| [c, reflection_store.category_score(c)] }
|
|
23
|
+
llm_result = Helpers::LlmEnhancer.enhance_reflection(
|
|
24
|
+
monitors_data: new_reflections,
|
|
25
|
+
health_scores: health_scores
|
|
26
|
+
)
|
|
27
|
+
if llm_result
|
|
28
|
+
new_reflections.each do |entry|
|
|
29
|
+
enhanced = llm_result[:observations][entry[:category]]
|
|
30
|
+
next unless enhanced
|
|
31
|
+
|
|
32
|
+
entry[:observation] = enhanced
|
|
33
|
+
entry[:source] = :llm
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Legion::Logging.debug "[reflection] generated #{new_reflections.size} reflections, health=#{reflection_store.cognitive_health}"
|
|
39
|
+
|
|
40
|
+
{
|
|
41
|
+
reflections_generated: new_reflections.size,
|
|
42
|
+
cognitive_health: reflection_store.cognitive_health,
|
|
43
|
+
new_reflections: new_reflections.map { |r| format_reflection(r) },
|
|
44
|
+
total_reflections: reflection_store.count
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def reflect_on_dream(dream_results: {}, **)
|
|
49
|
+
source = :mechanical
|
|
50
|
+
reflection = nil
|
|
51
|
+
|
|
52
|
+
if Helpers::LlmEnhancer.available?
|
|
53
|
+
llm_result = Helpers::LlmEnhancer.reflect_on_dream(dream_results: dream_results)
|
|
54
|
+
if llm_result&.fetch(:reflection, nil)
|
|
55
|
+
reflection = llm_result[:reflection]
|
|
56
|
+
source = :llm
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
reflection ||= build_mechanical_dream_reflection(dream_results)
|
|
61
|
+
|
|
62
|
+
Legion::Logging.debug "[reflection] dream reflection generated source=#{source}"
|
|
63
|
+
{ reflection: reflection, source: source }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def cognitive_health(**)
|
|
67
|
+
health = reflection_store.cognitive_health
|
|
68
|
+
Legion::Logging.debug "[reflection] cognitive health: #{health}"
|
|
69
|
+
{
|
|
70
|
+
health: health,
|
|
71
|
+
category_scores: Helpers::Constants::CATEGORIES.to_h { |c| [c, reflection_store.category_score(c)] },
|
|
72
|
+
unacted_count: reflection_store.unacted.size,
|
|
73
|
+
critical_count: reflection_store.by_severity(:critical).size,
|
|
74
|
+
significant_count: reflection_store.by_severity(:significant).size
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def recent_reflections(limit: 10, **)
|
|
79
|
+
reflections = reflection_store.recent(limit: limit)
|
|
80
|
+
{ reflections: reflections.map { |r| format_reflection(r) } }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def reflections_by_category(category:, **)
|
|
84
|
+
cat = category.to_sym
|
|
85
|
+
reflections = reflection_store.by_category(cat)
|
|
86
|
+
{ category: cat, reflections: reflections.map { |r| format_reflection(r) } }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def adapt(reflection_id:, **)
|
|
90
|
+
reflection = reflection_store.get(reflection_id)
|
|
91
|
+
return { error: :not_found } unless reflection
|
|
92
|
+
return { error: :already_acted } if reflection[:acted_on]
|
|
93
|
+
|
|
94
|
+
reflection_store.mark_acted_on(reflection_id)
|
|
95
|
+
Legion::Logging.info "[reflection] adapted: #{reflection[:observation]}"
|
|
96
|
+
{ adapted: true, reflection_id: reflection_id, recommendation: reflection[:recommendation] }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def reflection_stats(**)
|
|
100
|
+
{
|
|
101
|
+
total_generated: reflection_store.total_generated,
|
|
102
|
+
current_count: reflection_store.count,
|
|
103
|
+
cognitive_health: reflection_store.cognitive_health,
|
|
104
|
+
severity_counts: reflection_store.severity_counts,
|
|
105
|
+
category_counts: reflection_store.category_counts,
|
|
106
|
+
unacted: reflection_store.unacted.size
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def build_mechanical_dream_reflection(dream_results)
|
|
113
|
+
return 'Dream cycle completed.' unless dream_results.is_a?(Hash) && dream_results.any?
|
|
114
|
+
|
|
115
|
+
parts = []
|
|
116
|
+
if (audit = dream_results[:memory_audit]).is_a?(Hash)
|
|
117
|
+
parts << "Memory audit: #{audit[:decayed] || 0} traces decayed, #{audit[:unresolved_count] || 0} unresolved."
|
|
118
|
+
end
|
|
119
|
+
if (contra = dream_results[:contradiction_resolution]).is_a?(Hash)
|
|
120
|
+
parts << "Contradictions: #{contra[:detected] || 0} detected, #{contra[:resolved] || 0} resolved."
|
|
121
|
+
end
|
|
122
|
+
if (agenda = dream_results[:agenda_formation]).is_a?(Hash)
|
|
123
|
+
parts << "Agenda formed with #{agenda[:agenda_items] || 0} items."
|
|
124
|
+
end
|
|
125
|
+
parts.empty? ? 'Dream cycle completed.' : parts.join(' ')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def reflection_store
|
|
129
|
+
@reflection_store ||= Helpers::ReflectionStore.new
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def format_reflection(reflection)
|
|
133
|
+
{
|
|
134
|
+
reflection_id: reflection[:reflection_id],
|
|
135
|
+
category: reflection[:category],
|
|
136
|
+
observation: reflection[:observation],
|
|
137
|
+
severity: reflection[:severity],
|
|
138
|
+
recommendation: reflection[:recommendation],
|
|
139
|
+
acted_on: reflection[:acted_on],
|
|
140
|
+
created_at: reflection[:created_at]
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def update_category_scores(tick_results)
|
|
145
|
+
update_prediction_score(tick_results)
|
|
146
|
+
update_curiosity_score(tick_results)
|
|
147
|
+
update_emotion_score(tick_results)
|
|
148
|
+
update_memory_score(tick_results)
|
|
149
|
+
update_load_score(tick_results)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def update_prediction_score(tick_results)
|
|
153
|
+
prediction = tick_results[:prediction_engine]
|
|
154
|
+
return unless prediction.is_a?(Hash) && prediction[:confidence].is_a?(Numeric)
|
|
155
|
+
|
|
156
|
+
reflection_store.update_category_score(:prediction_calibration, prediction[:confidence])
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def update_curiosity_score(tick_results)
|
|
160
|
+
curiosity = tick_results[:working_memory_integration]
|
|
161
|
+
return unless curiosity.is_a?(Hash) && curiosity[:curiosity_intensity].is_a?(Numeric)
|
|
162
|
+
|
|
163
|
+
score = 1.0 - ([curiosity[:curiosity_intensity], 1.0].min * 0.3)
|
|
164
|
+
reflection_store.update_category_score(:curiosity_effectiveness, score)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def update_emotion_score(tick_results)
|
|
168
|
+
emotion = tick_results[:emotional_evaluation]
|
|
169
|
+
stability = emotion.is_a?(Hash) ? (emotion[:stability] || emotion.dig(:momentum, :stability)) : nil
|
|
170
|
+
return unless stability.is_a?(Numeric)
|
|
171
|
+
|
|
172
|
+
reflection_store.update_category_score(:emotional_stability, stability)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def update_memory_score(tick_results)
|
|
176
|
+
memory = tick_results[:memory_consolidation]
|
|
177
|
+
return unless memory.is_a?(Hash) && memory[:total].is_a?(Numeric) && memory[:total].positive?
|
|
178
|
+
|
|
179
|
+
ratio = (memory[:pruned] || 0).to_f / memory[:total]
|
|
180
|
+
reflection_store.update_category_score(:memory_health, 1.0 - ratio)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def update_load_score(tick_results)
|
|
184
|
+
elapsed = tick_results[:elapsed]
|
|
185
|
+
budget = tick_results[:budget]
|
|
186
|
+
return unless elapsed.is_a?(Numeric) && budget.is_a?(Numeric) && budget.positive?
|
|
187
|
+
|
|
188
|
+
utilization = elapsed / budget
|
|
189
|
+
reflection_store.update_category_score(:cognitive_load, [1.0 - utilization, 0.0].max)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/reflection/version'
|
|
4
|
+
require 'legion/extensions/reflection/helpers/constants'
|
|
5
|
+
require 'legion/extensions/reflection/helpers/reflection'
|
|
6
|
+
require 'legion/extensions/reflection/helpers/reflection_store'
|
|
7
|
+
require 'legion/extensions/reflection/helpers/monitors'
|
|
8
|
+
require 'legion/extensions/reflection/helpers/llm_enhancer'
|
|
9
|
+
require 'legion/extensions/reflection/runners/reflection'
|
|
10
|
+
require 'legion/extensions/reflection/client'
|
|
11
|
+
|
|
12
|
+
module Legion
|
|
13
|
+
module Extensions
|
|
14
|
+
module Reflection
|
|
15
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Reflection::Client do
|
|
4
|
+
subject(:client) { described_class.new }
|
|
5
|
+
|
|
6
|
+
it 'initializes with a default reflection store' do
|
|
7
|
+
expect(client.reflection_store).to be_a(Legion::Extensions::Reflection::Helpers::ReflectionStore)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it 'accepts an injected store' do
|
|
11
|
+
custom = Legion::Extensions::Reflection::Helpers::ReflectionStore.new
|
|
12
|
+
client = described_class.new(store: custom)
|
|
13
|
+
expect(client.reflection_store).to be(custom)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'includes the Reflection runner' do
|
|
17
|
+
expect(client).to respond_to(:reflect)
|
|
18
|
+
expect(client).to respond_to(:cognitive_health)
|
|
19
|
+
expect(client).to respond_to(:recent_reflections)
|
|
20
|
+
expect(client).to respond_to(:reflections_by_category)
|
|
21
|
+
expect(client).to respond_to(:adapt)
|
|
22
|
+
expect(client).to respond_to(:reflection_stats)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Reflection::Helpers::LlmEnhancer do
|
|
4
|
+
describe '.available?' do
|
|
5
|
+
context 'when Legion::LLM is not defined' do
|
|
6
|
+
it 'returns a falsy value' do
|
|
7
|
+
# Legion::LLM is not defined in the test environment
|
|
8
|
+
expect(described_class.available?).to be_falsy
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
context 'when Legion::LLM is defined but not started' do
|
|
13
|
+
before do
|
|
14
|
+
stub_const('Legion::LLM', double(respond_to?: true, started?: false))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'returns false' do
|
|
18
|
+
expect(described_class.available?).to be false
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
context 'when Legion::LLM is started' do
|
|
23
|
+
before do
|
|
24
|
+
stub_const('Legion::LLM', double(respond_to?: true, started?: true))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'returns true' do
|
|
28
|
+
expect(described_class.available?).to be true
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
context 'when an error is raised' do
|
|
33
|
+
before do
|
|
34
|
+
stub_const('Legion::LLM', double)
|
|
35
|
+
allow(Legion::LLM).to receive(:respond_to?).and_raise(StandardError)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'returns false' do
|
|
39
|
+
expect(described_class.available?).to be false
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe '.enhance_reflection' do
|
|
45
|
+
let(:fake_response) do
|
|
46
|
+
double(content: <<~TEXT)
|
|
47
|
+
EMOTION: My arousal is elevated at 0.7 while stability holds at 0.8, suggesting urgency without destabilization.
|
|
48
|
+
PREDICTION: Confidence at 65% with a declining trend and 3 pending predictions signals I am losing ground in forward modeling.
|
|
49
|
+
MEMORY: Memory health appears nominal with no concerning decay patterns detected this cycle.
|
|
50
|
+
TRUST: Trust scores remain stable with no significant drift observed.
|
|
51
|
+
CURIOSITY: Curiosity intensity is moderate; resolution rates suggest effective exploration.
|
|
52
|
+
IDENTITY: Identity entropy is within expected bounds; no drift detected.
|
|
53
|
+
TEXT
|
|
54
|
+
end
|
|
55
|
+
let(:fake_chat) { double }
|
|
56
|
+
let(:monitors_data) do
|
|
57
|
+
[
|
|
58
|
+
{
|
|
59
|
+
category: :emotional_stability,
|
|
60
|
+
observation: 'original',
|
|
61
|
+
severity: :notable,
|
|
62
|
+
metrics: { stability: 0.8 },
|
|
63
|
+
recommendation: :no_action
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
category: :prediction_calibration,
|
|
67
|
+
observation: 'original',
|
|
68
|
+
severity: :notable,
|
|
69
|
+
metrics: { confidence: 0.65 },
|
|
70
|
+
recommendation: :increase_curiosity
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
end
|
|
74
|
+
let(:health_scores) do
|
|
75
|
+
{
|
|
76
|
+
prediction_calibration: 0.65,
|
|
77
|
+
curiosity_effectiveness: 0.8,
|
|
78
|
+
emotional_stability: 0.8,
|
|
79
|
+
trust_drift: 1.0,
|
|
80
|
+
memory_health: 0.95,
|
|
81
|
+
cognitive_load: 0.9,
|
|
82
|
+
mode_patterns: 1.0
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
before do
|
|
87
|
+
stub_const('Legion::LLM', double)
|
|
88
|
+
allow(Legion::LLM).to receive(:chat).and_return(fake_chat)
|
|
89
|
+
allow(fake_chat).to receive(:with_instructions)
|
|
90
|
+
allow(fake_chat).to receive(:ask).and_return(fake_response)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'returns observations hash with at least some categories' do
|
|
94
|
+
result = described_class.enhance_reflection(
|
|
95
|
+
monitors_data: monitors_data,
|
|
96
|
+
health_scores: health_scores
|
|
97
|
+
)
|
|
98
|
+
expect(result).to be_a(Hash)
|
|
99
|
+
expect(result[:observations]).to be_a(Hash)
|
|
100
|
+
expect(result[:observations]).not_to be_empty
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'parses per-category observation text' do
|
|
104
|
+
result = described_class.enhance_reflection(
|
|
105
|
+
monitors_data: monitors_data,
|
|
106
|
+
health_scores: health_scores
|
|
107
|
+
)
|
|
108
|
+
expect(result[:observations][:emotional_stability]).to include('arousal')
|
|
109
|
+
expect(result[:observations][:prediction_calibration]).to include('Confidence')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
context 'when LLM returns nil content' do
|
|
113
|
+
before { allow(fake_chat).to receive(:ask).and_return(double(content: nil)) }
|
|
114
|
+
|
|
115
|
+
it 'returns nil' do
|
|
116
|
+
result = described_class.enhance_reflection(
|
|
117
|
+
monitors_data: monitors_data,
|
|
118
|
+
health_scores: health_scores
|
|
119
|
+
)
|
|
120
|
+
expect(result).to be_nil
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
context 'when LLM raises an error' do
|
|
125
|
+
before { allow(fake_chat).to receive(:ask).and_raise(StandardError, 'LLM timeout') }
|
|
126
|
+
|
|
127
|
+
it 'returns nil and logs a warning' do
|
|
128
|
+
expect(Legion::Logging).to receive(:warn).with(/enhance_reflection failed/)
|
|
129
|
+
result = described_class.enhance_reflection(
|
|
130
|
+
monitors_data: monitors_data,
|
|
131
|
+
health_scores: health_scores
|
|
132
|
+
)
|
|
133
|
+
expect(result).to be_nil
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
describe '.reflect_on_dream' do
|
|
139
|
+
let(:fake_response) do
|
|
140
|
+
double(content: <<~TEXT)
|
|
141
|
+
REFLECTION: The dream cycle surfaced 3 unresolved traces and resolved 1 contradiction. Memory consolidation is progressing normally with agenda items focused on identity coherence.
|
|
142
|
+
TEXT
|
|
143
|
+
end
|
|
144
|
+
let(:fake_chat) { double }
|
|
145
|
+
let(:dream_results) do
|
|
146
|
+
{
|
|
147
|
+
memory_audit: { decayed: 5, pruned: 2, unresolved_count: 3 },
|
|
148
|
+
contradiction_resolution: { detected: 2, resolved: 1 },
|
|
149
|
+
agenda_formation: { agenda_items: 4 }
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
before do
|
|
154
|
+
stub_const('Legion::LLM', double)
|
|
155
|
+
allow(Legion::LLM).to receive(:chat).and_return(fake_chat)
|
|
156
|
+
allow(fake_chat).to receive(:with_instructions)
|
|
157
|
+
allow(fake_chat).to receive(:ask).and_return(fake_response)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it 'returns a reflection string' do
|
|
161
|
+
result = described_class.reflect_on_dream(dream_results: dream_results)
|
|
162
|
+
expect(result).to be_a(Hash)
|
|
163
|
+
expect(result[:reflection]).to be_a(String)
|
|
164
|
+
expect(result[:reflection]).not_to be_empty
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'includes dream cycle content in the reflection' do
|
|
168
|
+
result = described_class.reflect_on_dream(dream_results: dream_results)
|
|
169
|
+
expect(result[:reflection]).to include('unresolved traces')
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
context 'when LLM returns nil content' do
|
|
173
|
+
before { allow(fake_chat).to receive(:ask).and_return(double(content: nil)) }
|
|
174
|
+
|
|
175
|
+
it 'returns nil' do
|
|
176
|
+
result = described_class.reflect_on_dream(dream_results: dream_results)
|
|
177
|
+
expect(result).to be_nil
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
context 'when LLM raises an error' do
|
|
182
|
+
before { allow(fake_chat).to receive(:ask).and_raise(StandardError, 'model error') }
|
|
183
|
+
|
|
184
|
+
it 'returns nil and logs a warning' do
|
|
185
|
+
expect(Legion::Logging).to receive(:warn).with(/reflect_on_dream failed/)
|
|
186
|
+
result = described_class.reflect_on_dream(dream_results: dream_results)
|
|
187
|
+
expect(result).to be_nil
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Reflection::Helpers::Monitors do
|
|
4
|
+
describe '.monitor_predictions' do
|
|
5
|
+
it 'generates reflection for low confidence' do
|
|
6
|
+
results = described_class.monitor_predictions(
|
|
7
|
+
{ prediction_engine: { confidence: 0.2 } },
|
|
8
|
+
[]
|
|
9
|
+
)
|
|
10
|
+
expect(results.size).to be >= 1
|
|
11
|
+
expect(results.first[:category]).to eq(:prediction_calibration)
|
|
12
|
+
expect(results.first[:recommendation]).to eq(:increase_curiosity)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'returns empty for good confidence' do
|
|
16
|
+
results = described_class.monitor_predictions(
|
|
17
|
+
{ prediction_engine: { confidence: 0.9 } },
|
|
18
|
+
[]
|
|
19
|
+
)
|
|
20
|
+
expect(results).to be_empty
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'detects accuracy trend drop' do
|
|
24
|
+
history = Array.new(5) { { prediction_engine: { confidence: 0.9 } } } +
|
|
25
|
+
Array.new(5) { { prediction_engine: { confidence: 0.5 } } }
|
|
26
|
+
|
|
27
|
+
results = described_class.monitor_predictions(
|
|
28
|
+
{ prediction_engine: { confidence: 0.5 } },
|
|
29
|
+
history
|
|
30
|
+
)
|
|
31
|
+
trend = results.find { |r| r[:metrics][:trend_drop] }
|
|
32
|
+
expect(trend).not_to be_nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe '.monitor_curiosity' do
|
|
37
|
+
it 'generates reflection for low resolution rate' do
|
|
38
|
+
results = described_class.monitor_curiosity(
|
|
39
|
+
working_memory_integration: { resolution_rate: 0.1 }
|
|
40
|
+
)
|
|
41
|
+
expect(results.size).to eq(1)
|
|
42
|
+
expect(results.first[:recommendation]).to eq(:decrease_curiosity)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'celebrates high resolution rate' do
|
|
46
|
+
results = described_class.monitor_curiosity(
|
|
47
|
+
working_memory_integration: { resolution_rate: 0.9 }
|
|
48
|
+
)
|
|
49
|
+
expect(results.size).to eq(1)
|
|
50
|
+
expect(results.first[:recommendation]).to eq(:celebrate_success)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe '.monitor_emotions' do
|
|
55
|
+
it 'generates reflection for instability' do
|
|
56
|
+
results = described_class.monitor_emotions(
|
|
57
|
+
emotional_evaluation: { stability: 0.1 }
|
|
58
|
+
)
|
|
59
|
+
expect(results.size).to eq(1)
|
|
60
|
+
expect(results.first[:category]).to eq(:emotional_stability)
|
|
61
|
+
expect(results.first[:severity]).to eq(:significant)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'detects emotional flatness' do
|
|
65
|
+
results = described_class.monitor_emotions(
|
|
66
|
+
emotional_evaluation: { stability: 0.99 }
|
|
67
|
+
)
|
|
68
|
+
expect(results.size).to eq(1)
|
|
69
|
+
expect(results.first[:observation]).to include('flat')
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe '.monitor_memory' do
|
|
74
|
+
it 'generates reflection for high decay ratio' do
|
|
75
|
+
results = described_class.monitor_memory(
|
|
76
|
+
memory_consolidation: { pruned: 90, total: 100 }
|
|
77
|
+
)
|
|
78
|
+
expect(results.size).to eq(1)
|
|
79
|
+
expect(results.first[:recommendation]).to eq(:consolidate_memory)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'returns empty for healthy memory' do
|
|
83
|
+
results = described_class.monitor_memory(
|
|
84
|
+
memory_consolidation: { pruned: 5, total: 100 }
|
|
85
|
+
)
|
|
86
|
+
expect(results).to be_empty
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe '.monitor_cognitive_load' do
|
|
91
|
+
it 'generates reflection when near budget' do
|
|
92
|
+
results = described_class.monitor_cognitive_load(
|
|
93
|
+
elapsed: 4.8, budget: 5.0
|
|
94
|
+
)
|
|
95
|
+
expect(results.size).to eq(1)
|
|
96
|
+
expect(results.first[:category]).to eq(:cognitive_load)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'returns empty when within budget' do
|
|
100
|
+
results = described_class.monitor_cognitive_load(
|
|
101
|
+
elapsed: 1.0, budget: 5.0
|
|
102
|
+
)
|
|
103
|
+
expect(results).to be_empty
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe '.run_all' do
|
|
108
|
+
it 'aggregates results from all monitors' do
|
|
109
|
+
tick_results = {
|
|
110
|
+
prediction_engine: { confidence: 0.2 },
|
|
111
|
+
emotional_evaluation: { stability: 0.1 },
|
|
112
|
+
memory_consolidation: { pruned: 90, total: 100 },
|
|
113
|
+
elapsed: 4.8,
|
|
114
|
+
budget: 5.0
|
|
115
|
+
}
|
|
116
|
+
results = described_class.run_all(tick_results, [])
|
|
117
|
+
expect(results.size).to be >= 3
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|