lex-appraisal 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 +15 -0
- data/LICENSE +21 -0
- data/README.md +65 -0
- data/lex-appraisal.gemspec +29 -0
- data/lib/legion/extensions/appraisal/client.rb +16 -0
- data/lib/legion/extensions/appraisal/helpers/appraisal.rb +108 -0
- data/lib/legion/extensions/appraisal/helpers/appraisal_engine.rb +125 -0
- data/lib/legion/extensions/appraisal/helpers/constants.rb +39 -0
- data/lib/legion/extensions/appraisal/runners/appraisal.rb +101 -0
- data/lib/legion/extensions/appraisal/version.rb +9 -0
- data/lib/legion/extensions/appraisal.rb +15 -0
- data/spec/legion/extensions/appraisal/client_spec.rb +52 -0
- data/spec/legion/extensions/appraisal/helpers/appraisal_engine_spec.rb +161 -0
- data/spec/legion/extensions/appraisal/helpers/appraisal_spec.rb +175 -0
- data/spec/legion/extensions/appraisal/helpers/constants_spec.rb +49 -0
- data/spec/legion/extensions/appraisal/runners/appraisal_spec.rb +116 -0
- data/spec/spec_helper.rb +24 -0
- metadata +77 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 52c53808a09c9610ddad5a0ac61a10580160c2c0c1f45dcffaaeafc88e0a3e15
|
|
4
|
+
data.tar.gz: 805206fecd29f46906a9b9ff6ae7c62910f506ac8a6c0dddf161856ce0bd2613
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9413416882b9fa097d1f70cab841849924e0e84055aa2533b601f9144cc41b3d16585803687f8aea9fe556c58723ce0d38ca7c49a40637053d63a57611164625
|
|
7
|
+
data.tar.gz: 5efcd44babd9b35f8b8a914207a40bab734a9bae3a5fd66cca7aa8985e86f543e0ad9243ec9d88a1fa70a6f6123712e65539acaf5a29b4c5cf0fbd1bf96e6035
|
data/Gemfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
|
+
gemspec
|
|
5
|
+
|
|
6
|
+
group :test do
|
|
7
|
+
gem 'rake'
|
|
8
|
+
gem 'rspec'
|
|
9
|
+
gem 'rspec_junit_formatter'
|
|
10
|
+
gem 'rubocop', require: false
|
|
11
|
+
gem 'rubocop-rspec', require: false
|
|
12
|
+
gem 'simplecov'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
gem 'legion-gaia', path: '../../legion-gaia'
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthew Iverson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# lex-appraisal
|
|
2
|
+
|
|
3
|
+
Lazarus's Cognitive Appraisal Theory for brain-modeled agentic AI.
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
Models how the agent evaluates events to determine their emotional significance. Based on Lazarus's two-stage appraisal process: primary appraisal assesses relevance, goal alignment, and importance; secondary appraisal assesses coping capacity, control, and future expectancy. The combination determines the emotional response (anxiety, joy, anger, challenge, etc.) and guides coping strategy selection.
|
|
8
|
+
|
|
9
|
+
## Core Concept: Two-Stage Appraisal
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# Stage 1: Primary appraisal (how relevant is this event to my goals?)
|
|
13
|
+
# Stage 2: Secondary appraisal (can I handle it?)
|
|
14
|
+
# Combined -> emotional response
|
|
15
|
+
|
|
16
|
+
result = client.appraise_event(
|
|
17
|
+
event: 'service outage in production',
|
|
18
|
+
primary: { relevance: 0.95, goal_congruence: -0.8, goal_importance: 0.9 },
|
|
19
|
+
secondary: { coping_potential: 0.3, control_expectation: 0.4, future_expectancy: 0.5 },
|
|
20
|
+
domain: :infrastructure
|
|
21
|
+
)
|
|
22
|
+
# => { appraisal: { emotion: :anxiety, intensity: 0.8, ... } }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
client = Legion::Extensions::Appraisal::Client.new
|
|
29
|
+
|
|
30
|
+
# Register a coping strategy
|
|
31
|
+
client.add_coping_strategy(name: :incident_playbook, coping_type: :problem_focused, effectiveness: 0.85)
|
|
32
|
+
|
|
33
|
+
# Appraise an event
|
|
34
|
+
result = client.appraise_event(event: 'deployment failed', primary: {...}, secondary: {...})
|
|
35
|
+
|
|
36
|
+
# Reappraise with new information (reduces intensity by 30%)
|
|
37
|
+
client.reappraise_event(
|
|
38
|
+
appraisal_id: result[:appraisal][:id],
|
|
39
|
+
new_primary: { relevance: 0.5, goal_congruence: 0.2, goal_importance: 0.6 },
|
|
40
|
+
new_secondary: { coping_potential: 0.8, control_expectation: 0.7, future_expectancy: 0.8 }
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Select a coping strategy
|
|
44
|
+
client.select_coping_strategy(appraisal_id: id, coping_type: :problem_focused)
|
|
45
|
+
|
|
46
|
+
# View emotional patterns
|
|
47
|
+
client.emotional_pattern
|
|
48
|
+
# => { anxiety: 3, challenge: 2, joy: 1 }
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Integration
|
|
52
|
+
|
|
53
|
+
Feeds into lex-emotion: derived emotions provide typed signals for valence evaluation. Reappraisal models cognitive emotion regulation. `emotional_pattern` informs lex-dream's agenda formation about recurring stressors.
|
|
54
|
+
|
|
55
|
+
## Development
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
bundle install
|
|
59
|
+
bundle exec rspec
|
|
60
|
+
bundle exec rubocop
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/appraisal/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-appraisal'
|
|
7
|
+
spec.version = Legion::Extensions::Appraisal::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Appraisal'
|
|
12
|
+
spec.description = "Lazarus's Cognitive Appraisal Theory for brain-modeled agentic AI"
|
|
13
|
+
spec.homepage = 'https://github.com/LegionIO/lex-appraisal'
|
|
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-appraisal'
|
|
19
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-appraisal'
|
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-appraisal'
|
|
21
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-appraisal/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-appraisal.gemspec Gemfile LICENSE README.md]
|
|
26
|
+
end
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
spec.add_development_dependency 'legion-gaia'
|
|
29
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/appraisal/helpers/constants'
|
|
4
|
+
require 'legion/extensions/appraisal/helpers/appraisal'
|
|
5
|
+
require 'legion/extensions/appraisal/helpers/appraisal_engine'
|
|
6
|
+
require 'legion/extensions/appraisal/runners/appraisal'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Extensions
|
|
10
|
+
module Appraisal
|
|
11
|
+
class Client
|
|
12
|
+
include Runners::Appraisal
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Appraisal
|
|
8
|
+
module Helpers
|
|
9
|
+
class Appraisal
|
|
10
|
+
include Constants
|
|
11
|
+
|
|
12
|
+
attr_reader :id, :event, :domain, :primary, :secondary,
|
|
13
|
+
:emotional_outcome, :intensity, :coping_strategy,
|
|
14
|
+
:reappraised, :created_at, :reappraised_at
|
|
15
|
+
|
|
16
|
+
def initialize(event:, primary:, secondary:, domain: nil)
|
|
17
|
+
@id = SecureRandom.uuid
|
|
18
|
+
@event = event
|
|
19
|
+
@domain = domain
|
|
20
|
+
@primary = normalize_dimensions(primary, PRIMARY_DIMENSIONS)
|
|
21
|
+
@secondary = normalize_dimensions(secondary, SECONDARY_DIMENSIONS)
|
|
22
|
+
@intensity = DEFAULT_INTENSITY
|
|
23
|
+
@coping_strategy = nil
|
|
24
|
+
@reappraised = false
|
|
25
|
+
@created_at = Time.now.utc
|
|
26
|
+
@reappraised_at = nil
|
|
27
|
+
@emotional_outcome = compute_emotion
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def reappraise(new_primary:, new_secondary:)
|
|
31
|
+
@primary = normalize_dimensions(new_primary, PRIMARY_DIMENSIONS)
|
|
32
|
+
@secondary = normalize_dimensions(new_secondary, SECONDARY_DIMENSIONS)
|
|
33
|
+
@emotional_outcome = compute_emotion
|
|
34
|
+
@intensity = (@intensity * (1 - REAPPRAISAL_DISCOUNT)).clamp(INTENSITY_FLOOR, INTENSITY_CEILING)
|
|
35
|
+
@reappraised = true
|
|
36
|
+
@reappraised_at = Time.now.utc
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def compute_emotion
|
|
41
|
+
relevance = @primary[:relevance]
|
|
42
|
+
goal_congruence = @primary[:goal_congruence]
|
|
43
|
+
coping_potential = @secondary[:coping_potential]
|
|
44
|
+
|
|
45
|
+
return :indifference if relevance < 0.3
|
|
46
|
+
|
|
47
|
+
classify_emotion(goal_congruence, coping_potential)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_h
|
|
51
|
+
{
|
|
52
|
+
id: @id,
|
|
53
|
+
event: @event,
|
|
54
|
+
domain: @domain,
|
|
55
|
+
primary: @primary,
|
|
56
|
+
secondary: @secondary,
|
|
57
|
+
emotional_outcome: @emotional_outcome,
|
|
58
|
+
intensity: @intensity,
|
|
59
|
+
coping_strategy: @coping_strategy,
|
|
60
|
+
reappraised: @reappraised,
|
|
61
|
+
created_at: @created_at,
|
|
62
|
+
reappraised_at: @reappraised_at
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def assign_coping(strategy_name)
|
|
67
|
+
@coping_strategy = strategy_name
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def decay!
|
|
71
|
+
@intensity = (@intensity - DECAY_RATE).clamp(INTENSITY_FLOOR, INTENSITY_CEILING)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def classify_emotion(goal_congruence, coping_potential)
|
|
77
|
+
if goal_congruence < 0.4
|
|
78
|
+
low_congruence_emotion(goal_congruence, coping_potential)
|
|
79
|
+
elsif goal_congruence > 0.7
|
|
80
|
+
:joy
|
|
81
|
+
else
|
|
82
|
+
:sadness
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def low_congruence_emotion(goal_congruence, coping_potential)
|
|
87
|
+
if goal_congruence < 0.3
|
|
88
|
+
:anger
|
|
89
|
+
elsif coping_potential < 0.4
|
|
90
|
+
:anxiety
|
|
91
|
+
elsif coping_potential >= 0.6
|
|
92
|
+
:challenge
|
|
93
|
+
else
|
|
94
|
+
:sadness
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def normalize_dimensions(raw, dimensions)
|
|
99
|
+
dimensions.to_h do |dim|
|
|
100
|
+
val = raw.fetch(dim, 0.0).to_f
|
|
101
|
+
[dim, val.clamp(INTENSITY_FLOOR, INTENSITY_CEILING)]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Appraisal
|
|
6
|
+
module Helpers
|
|
7
|
+
class AppraisalEngine
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@appraisals = {}
|
|
12
|
+
@coping_strategies = {}
|
|
13
|
+
@history = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def appraise(event:, primary:, secondary:, domain: nil)
|
|
17
|
+
record = Appraisal.new(event: event, primary: primary, secondary: secondary, domain: domain)
|
|
18
|
+
prune_appraisals if @appraisals.size >= MAX_APPRAISALS
|
|
19
|
+
@appraisals[record.id] = record
|
|
20
|
+
archive(record)
|
|
21
|
+
record
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def reappraise(appraisal_id:, new_primary:, new_secondary:)
|
|
25
|
+
record = @appraisals[appraisal_id]
|
|
26
|
+
return nil unless record
|
|
27
|
+
|
|
28
|
+
record.reappraise(new_primary: new_primary, new_secondary: new_secondary)
|
|
29
|
+
archive(record)
|
|
30
|
+
record
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def select_coping(appraisal_id:, coping_type:)
|
|
34
|
+
record = @appraisals[appraisal_id]
|
|
35
|
+
return nil unless record
|
|
36
|
+
|
|
37
|
+
strategy = find_best_strategy(coping_type)
|
|
38
|
+
name = strategy ? strategy[:name] : coping_type.to_s
|
|
39
|
+
record.assign_coping(name)
|
|
40
|
+
record
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def add_coping_strategy(name:, coping_type:, effectiveness:)
|
|
44
|
+
return false if @coping_strategies.size >= MAX_COPING_STRATEGIES
|
|
45
|
+
|
|
46
|
+
@coping_strategies[name] = {
|
|
47
|
+
name: name,
|
|
48
|
+
coping_type: coping_type,
|
|
49
|
+
effectiveness: effectiveness.to_f.clamp(INTENSITY_FLOOR, INTENSITY_CEILING)
|
|
50
|
+
}
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def evaluate_coping(appraisal_id:)
|
|
55
|
+
record = @appraisals[appraisal_id]
|
|
56
|
+
return { effectiveness: 0.0, resolved: false } unless record
|
|
57
|
+
return { effectiveness: 0.0, resolved: false } unless record.coping_strategy
|
|
58
|
+
|
|
59
|
+
strategy = @coping_strategies[record.coping_strategy]
|
|
60
|
+
base = strategy ? strategy[:effectiveness] : DEFAULT_INTENSITY
|
|
61
|
+
resolved = record.intensity < 0.3
|
|
62
|
+
{
|
|
63
|
+
appraisal_id: appraisal_id,
|
|
64
|
+
coping: record.coping_strategy,
|
|
65
|
+
effectiveness: base,
|
|
66
|
+
intensity: record.intensity,
|
|
67
|
+
resolved: resolved
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def by_emotion(emotion:)
|
|
72
|
+
@appraisals.values.select { |rec| rec.emotional_outcome == emotion }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def by_domain(domain:)
|
|
76
|
+
@appraisals.values.select { |rec| rec.domain == domain }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def unresolved
|
|
80
|
+
@appraisals.values.select { |rec| rec.coping_strategy.nil? }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def emotional_pattern
|
|
84
|
+
counts = Hash.new(0)
|
|
85
|
+
recent_appraisals.each { |rec| counts[rec.emotional_outcome] += 1 }
|
|
86
|
+
counts.sort_by { |_, cnt| -cnt }.to_h
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def decay_all
|
|
90
|
+
@appraisals.each_value(&:decay!)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def to_h
|
|
94
|
+
{
|
|
95
|
+
appraisals: @appraisals.transform_values(&:to_h),
|
|
96
|
+
coping_strategies: @coping_strategies,
|
|
97
|
+
history_size: @history.size
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def archive(record)
|
|
104
|
+
@history << { id: record.id, emotion: record.emotional_outcome, at: Time.now.utc }
|
|
105
|
+
@history.shift while @history.size > MAX_HISTORY
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def prune_appraisals
|
|
109
|
+
oldest_key = @appraisals.min_by { |_, rec| rec.created_at }&.first
|
|
110
|
+
@appraisals.delete(oldest_key) if oldest_key
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def find_best_strategy(coping_type)
|
|
114
|
+
matches = @coping_strategies.values.select { |str| str[:coping_type] == coping_type }
|
|
115
|
+
matches.max_by { |str| str[:effectiveness] }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def recent_appraisals
|
|
119
|
+
@appraisals.values.last(50)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Appraisal
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
MAX_APPRAISALS = 200
|
|
9
|
+
MAX_COPING_STRATEGIES = 50
|
|
10
|
+
MAX_HISTORY = 300
|
|
11
|
+
|
|
12
|
+
DEFAULT_INTENSITY = 0.5
|
|
13
|
+
INTENSITY_FLOOR = 0.0
|
|
14
|
+
INTENSITY_CEILING = 1.0
|
|
15
|
+
|
|
16
|
+
DECAY_RATE = 0.02
|
|
17
|
+
REAPPRAISAL_DISCOUNT = 0.3
|
|
18
|
+
|
|
19
|
+
PRIMARY_DIMENSIONS = %i[relevance goal_congruence goal_importance].freeze
|
|
20
|
+
|
|
21
|
+
SECONDARY_DIMENSIONS = %i[coping_potential control_expectation future_expectancy].freeze
|
|
22
|
+
|
|
23
|
+
APPRAISAL_EMOTIONS = {
|
|
24
|
+
threat_low_coping: :anxiety,
|
|
25
|
+
threat_high_coping: :challenge,
|
|
26
|
+
loss: :sadness,
|
|
27
|
+
goal_incongruent: :anger,
|
|
28
|
+
goal_congruent: :joy,
|
|
29
|
+
irrelevant: :indifference,
|
|
30
|
+
unexpected_positive: :surprise,
|
|
31
|
+
moral_violation: :disgust
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
COPING_TYPES = %i[problem_focused emotion_focused meaning_focused avoidant social_support].freeze
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Appraisal
|
|
6
|
+
module Runners
|
|
7
|
+
module Appraisal
|
|
8
|
+
def appraise_event(event:, primary:, secondary:, domain: nil, **)
|
|
9
|
+
Legion::Logging.debug("[lex-appraisal] appraise_event event=#{event} domain=#{domain}")
|
|
10
|
+
record = engine.appraise(event: event, primary: primary, secondary: secondary, domain: domain)
|
|
11
|
+
{ success: true, appraisal: record.to_h }
|
|
12
|
+
rescue StandardError => e
|
|
13
|
+
Legion::Logging.error("[lex-appraisal] appraise_event error: #{e.message}")
|
|
14
|
+
{ success: false, error: e.message }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def reappraise_event(appraisal_id:, new_primary:, new_secondary:, **)
|
|
18
|
+
Legion::Logging.debug("[lex-appraisal] reappraise_event id=#{appraisal_id}")
|
|
19
|
+
record = engine.reappraise(appraisal_id: appraisal_id, new_primary: new_primary,
|
|
20
|
+
new_secondary: new_secondary)
|
|
21
|
+
return { success: false, error: 'appraisal not found' } unless record
|
|
22
|
+
|
|
23
|
+
{ success: true, appraisal: record.to_h }
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
Legion::Logging.error("[lex-appraisal] reappraise_event error: #{e.message}")
|
|
26
|
+
{ success: false, error: e.message }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def select_coping_strategy(appraisal_id:, coping_type:, **)
|
|
30
|
+
Legion::Logging.debug("[lex-appraisal] select_coping appraisal_id=#{appraisal_id}")
|
|
31
|
+
record = engine.select_coping(appraisal_id: appraisal_id, coping_type: coping_type)
|
|
32
|
+
return { success: false, error: 'appraisal not found' } unless record
|
|
33
|
+
|
|
34
|
+
{ success: true, appraisal: record.to_h }
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
Legion::Logging.error("[lex-appraisal] select_coping error: #{e.message}")
|
|
37
|
+
{ success: false, error: e.message }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def add_coping_strategy(name:, coping_type:, effectiveness:, **)
|
|
41
|
+
Legion::Logging.debug("[lex-appraisal] add_coping_strategy name=#{name}")
|
|
42
|
+
added = engine.add_coping_strategy(name: name, coping_type: coping_type, effectiveness: effectiveness)
|
|
43
|
+
{ success: added, name: name, coping_type: coping_type }
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
Legion::Logging.error("[lex-appraisal] add_coping_strategy error: #{e.message}")
|
|
46
|
+
{ success: false, error: e.message }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def evaluate_coping(appraisal_id:, **)
|
|
50
|
+
Legion::Logging.debug("[lex-appraisal] evaluate_coping id=#{appraisal_id}")
|
|
51
|
+
result = engine.evaluate_coping(appraisal_id: appraisal_id)
|
|
52
|
+
{ success: true }.merge(result)
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
Legion::Logging.error("[lex-appraisal] evaluate_coping error: #{e.message}")
|
|
55
|
+
{ success: false, error: e.message }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def emotional_pattern(**)
|
|
59
|
+
Legion::Logging.debug('[lex-appraisal] emotional_pattern')
|
|
60
|
+
pattern = engine.emotional_pattern
|
|
61
|
+
{ success: true, pattern: pattern }
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
Legion::Logging.error("[lex-appraisal] emotional_pattern error: #{e.message}")
|
|
64
|
+
{ success: false, error: e.message }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def update_appraisal(**)
|
|
68
|
+
Legion::Logging.debug('[lex-appraisal] update_appraisal (decay cycle)')
|
|
69
|
+
engine.decay_all
|
|
70
|
+
{ success: true }
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
Legion::Logging.error("[lex-appraisal] update_appraisal error: #{e.message}")
|
|
73
|
+
{ success: false, error: e.message }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def appraisal_stats(**)
|
|
77
|
+
Legion::Logging.debug('[lex-appraisal] appraisal_stats')
|
|
78
|
+
data = engine.to_h
|
|
79
|
+
unresolved = engine.unresolved.size
|
|
80
|
+
{
|
|
81
|
+
success: true,
|
|
82
|
+
total: data[:appraisals].size,
|
|
83
|
+
unresolved: unresolved,
|
|
84
|
+
history_size: data[:history_size],
|
|
85
|
+
pattern: engine.emotional_pattern
|
|
86
|
+
}
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
Legion::Logging.error("[lex-appraisal] appraisal_stats error: #{e.message}")
|
|
89
|
+
{ success: false, error: e.message }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def engine
|
|
95
|
+
@engine ||= Helpers::AppraisalEngine.new
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/appraisal/version'
|
|
4
|
+
require 'legion/extensions/appraisal/helpers/constants'
|
|
5
|
+
require 'legion/extensions/appraisal/helpers/appraisal'
|
|
6
|
+
require 'legion/extensions/appraisal/helpers/appraisal_engine'
|
|
7
|
+
require 'legion/extensions/appraisal/runners/appraisal'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module Appraisal
|
|
12
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/appraisal/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Appraisal::Client do
|
|
6
|
+
let(:client) { described_class.new }
|
|
7
|
+
|
|
8
|
+
it 'responds to all runner methods' do
|
|
9
|
+
expect(client).to respond_to(:appraise_event)
|
|
10
|
+
expect(client).to respond_to(:reappraise_event)
|
|
11
|
+
expect(client).to respond_to(:select_coping_strategy)
|
|
12
|
+
expect(client).to respond_to(:add_coping_strategy)
|
|
13
|
+
expect(client).to respond_to(:evaluate_coping)
|
|
14
|
+
expect(client).to respond_to(:emotional_pattern)
|
|
15
|
+
expect(client).to respond_to(:update_appraisal)
|
|
16
|
+
expect(client).to respond_to(:appraisal_stats)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'maintains isolated engine state per client instance' do
|
|
20
|
+
client2 = described_class.new
|
|
21
|
+
client.appraise_event(
|
|
22
|
+
event: 'only in client1',
|
|
23
|
+
primary: { relevance: 0.9, goal_congruence: 0.9, goal_importance: 0.8 },
|
|
24
|
+
secondary: { coping_potential: 0.8, control_expectation: 0.7, future_expectancy: 0.6 }
|
|
25
|
+
)
|
|
26
|
+
expect(client.appraisal_stats[:total]).to eq(1)
|
|
27
|
+
expect(client2.appraisal_stats[:total]).to eq(0)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'runs a full appraisal cycle' do
|
|
31
|
+
primary = { relevance: 0.9, goal_congruence: 0.3, goal_importance: 0.8 }
|
|
32
|
+
secondary = { coping_potential: 0.2, control_expectation: 0.3, future_expectancy: 0.4 }
|
|
33
|
+
|
|
34
|
+
appraisal_id = client.appraise_event(event: 'crisis', primary: primary,
|
|
35
|
+
secondary: secondary)[:appraisal][:id]
|
|
36
|
+
client.add_coping_strategy(name: 'deep_breathing', coping_type: :emotion_focused, effectiveness: 0.7)
|
|
37
|
+
client.select_coping_strategy(appraisal_id: appraisal_id, coping_type: :emotion_focused)
|
|
38
|
+
eval_result = client.evaluate_coping(appraisal_id: appraisal_id)
|
|
39
|
+
expect(eval_result[:success]).to be(true)
|
|
40
|
+
expect(eval_result[:coping]).to eq('deep_breathing')
|
|
41
|
+
|
|
42
|
+
client.reappraise_event(
|
|
43
|
+
appraisal_id: appraisal_id,
|
|
44
|
+
new_primary: { relevance: 0.9, goal_congruence: 0.9, goal_importance: 0.8 },
|
|
45
|
+
new_secondary: { coping_potential: 0.8, control_expectation: 0.7, future_expectancy: 0.6 }
|
|
46
|
+
)
|
|
47
|
+
client.update_appraisal
|
|
48
|
+
stats = client.appraisal_stats
|
|
49
|
+
expect(stats[:total]).to eq(1)
|
|
50
|
+
expect(stats[:pattern]).to be_a(Hash)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Appraisal::Helpers::AppraisalEngine do
|
|
4
|
+
let(:engine) { described_class.new }
|
|
5
|
+
|
|
6
|
+
let(:primary_joy) { { relevance: 0.9, goal_congruence: 0.9, goal_importance: 0.8 } }
|
|
7
|
+
let(:primary_threat) { { relevance: 0.9, goal_congruence: 0.3, goal_importance: 0.8 } }
|
|
8
|
+
let(:secondary_low) { { coping_potential: 0.2, control_expectation: 0.3, future_expectancy: 0.4 } }
|
|
9
|
+
let(:secondary_high) { { coping_potential: 0.8, control_expectation: 0.7, future_expectancy: 0.6 } }
|
|
10
|
+
|
|
11
|
+
describe '#appraise' do
|
|
12
|
+
it 'creates and returns an appraisal' do
|
|
13
|
+
record = engine.appraise(event: 'test', primary: primary_joy, secondary: secondary_high)
|
|
14
|
+
expect(record).to be_a(Legion::Extensions::Appraisal::Helpers::Appraisal)
|
|
15
|
+
expect(record.event).to eq('test')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'stores appraisals by id' do
|
|
19
|
+
record = engine.appraise(event: 'test', primary: primary_joy, secondary: secondary_high)
|
|
20
|
+
expect(engine.to_h[:appraisals]).to have_key(record.id)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'computes emotional_outcome' do
|
|
24
|
+
record = engine.appraise(event: 'test', primary: primary_joy, secondary: secondary_high)
|
|
25
|
+
expect(record.emotional_outcome).to eq(:joy)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe '#reappraise' do
|
|
30
|
+
it 'updates an existing appraisal' do
|
|
31
|
+
record = engine.appraise(event: 'test', primary: primary_threat, secondary: secondary_low)
|
|
32
|
+
expect(record.emotional_outcome).to eq(:anxiety)
|
|
33
|
+
updated = engine.reappraise(appraisal_id: record.id, new_primary: primary_joy, new_secondary: secondary_high)
|
|
34
|
+
expect(updated.emotional_outcome).to eq(:joy)
|
|
35
|
+
expect(updated.reappraised).to be(true)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'returns nil for unknown id' do
|
|
39
|
+
result = engine.reappraise(appraisal_id: 'unknown', new_primary: primary_joy, new_secondary: secondary_high)
|
|
40
|
+
expect(result).to be_nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe '#select_coping' do
|
|
45
|
+
it 'assigns coping to appraisal' do
|
|
46
|
+
record = engine.appraise(event: 'test', primary: primary_threat, secondary: secondary_low)
|
|
47
|
+
updated = engine.select_coping(appraisal_id: record.id, coping_type: :problem_focused)
|
|
48
|
+
expect(updated.coping_strategy).not_to be_nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'prefers registered strategies for the coping type' do
|
|
52
|
+
engine.add_coping_strategy(name: 'action_plan', coping_type: :problem_focused, effectiveness: 0.9)
|
|
53
|
+
record = engine.appraise(event: 'test', primary: primary_threat, secondary: secondary_low)
|
|
54
|
+
updated = engine.select_coping(appraisal_id: record.id, coping_type: :problem_focused)
|
|
55
|
+
expect(updated.coping_strategy).to eq('action_plan')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'returns nil for unknown appraisal' do
|
|
59
|
+
result = engine.select_coping(appraisal_id: 'unknown', coping_type: :problem_focused)
|
|
60
|
+
expect(result).to be_nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe '#add_coping_strategy' do
|
|
65
|
+
it 'registers a strategy and returns true' do
|
|
66
|
+
result = engine.add_coping_strategy(name: 'reframing', coping_type: :emotion_focused, effectiveness: 0.7)
|
|
67
|
+
expect(result).to be(true)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'clamps effectiveness to [0, 1]' do
|
|
71
|
+
engine.add_coping_strategy(name: 'over', coping_type: :problem_focused, effectiveness: 1.5)
|
|
72
|
+
data = engine.to_h
|
|
73
|
+
# Strategy stored (engine is internal, test via evaluate_coping behavior)
|
|
74
|
+
expect(data).to be_a(Hash)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe '#evaluate_coping' do
|
|
79
|
+
it 'returns effectiveness 0.0 when no coping assigned' do
|
|
80
|
+
record = engine.appraise(event: 'test', primary: primary_threat, secondary: secondary_low)
|
|
81
|
+
result = engine.evaluate_coping(appraisal_id: record.id)
|
|
82
|
+
expect(result[:effectiveness]).to eq(0.0)
|
|
83
|
+
expect(result[:resolved]).to be(false)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'uses registered strategy effectiveness' do
|
|
87
|
+
engine.add_coping_strategy(name: 'mindfulness', coping_type: :emotion_focused, effectiveness: 0.85)
|
|
88
|
+
record = engine.appraise(event: 'test', primary: primary_threat, secondary: secondary_low)
|
|
89
|
+
engine.select_coping(appraisal_id: record.id, coping_type: :emotion_focused)
|
|
90
|
+
result = engine.evaluate_coping(appraisal_id: record.id)
|
|
91
|
+
expect(result[:effectiveness]).to be_within(0.01).of(0.85)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'returns defaults for unknown appraisal' do
|
|
95
|
+
result = engine.evaluate_coping(appraisal_id: 'unknown')
|
|
96
|
+
expect(result[:effectiveness]).to eq(0.0)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe '#by_emotion' do
|
|
101
|
+
it 'filters appraisals by emotional outcome' do
|
|
102
|
+
engine.appraise(event: 'a', primary: primary_joy, secondary: secondary_high)
|
|
103
|
+
engine.appraise(event: 'b', primary: primary_threat, secondary: secondary_low)
|
|
104
|
+
joy_list = engine.by_emotion(emotion: :joy)
|
|
105
|
+
expect(joy_list.size).to eq(1)
|
|
106
|
+
expect(joy_list.first.event).to eq('a')
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
describe '#by_domain' do
|
|
111
|
+
it 'filters appraisals by domain' do
|
|
112
|
+
engine.appraise(event: 'a', primary: primary_joy, secondary: secondary_high, domain: 'work')
|
|
113
|
+
engine.appraise(event: 'b', primary: primary_joy, secondary: secondary_high, domain: 'personal')
|
|
114
|
+
work_list = engine.by_domain(domain: 'work')
|
|
115
|
+
expect(work_list.size).to eq(1)
|
|
116
|
+
expect(work_list.first.event).to eq('a')
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
describe '#unresolved' do
|
|
121
|
+
it 'returns appraisals without coping strategy' do
|
|
122
|
+
rec1 = engine.appraise(event: 'a', primary: primary_joy, secondary: secondary_high)
|
|
123
|
+
rec2 = engine.appraise(event: 'b', primary: primary_joy, secondary: secondary_high)
|
|
124
|
+
engine.select_coping(appraisal_id: rec1.id, coping_type: :problem_focused)
|
|
125
|
+
unresolved = engine.unresolved
|
|
126
|
+
expect(unresolved.map(&:id)).to include(rec2.id)
|
|
127
|
+
expect(unresolved.map(&:id)).not_to include(rec1.id)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
describe '#emotional_pattern' do
|
|
132
|
+
it 'returns emotion counts sorted by frequency' do
|
|
133
|
+
3.times { engine.appraise(event: 'a', primary: primary_joy, secondary: secondary_high) }
|
|
134
|
+
engine.appraise(event: 'b', primary: primary_threat, secondary: secondary_low)
|
|
135
|
+
pattern = engine.emotional_pattern
|
|
136
|
+
expect(pattern.first.first).to eq(:joy)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'returns empty hash when no appraisals' do
|
|
140
|
+
expect(engine.emotional_pattern).to eq({})
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
describe '#decay_all' do
|
|
145
|
+
it 'reduces intensity for all appraisals' do
|
|
146
|
+
rec = engine.appraise(event: 'test', primary: primary_joy, secondary: secondary_high)
|
|
147
|
+
engine.decay_all
|
|
148
|
+
expect(rec.intensity).to be < 0.5
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
describe '#to_h' do
|
|
153
|
+
it 'returns hash with appraisals, coping_strategies, history_size' do
|
|
154
|
+
engine.appraise(event: 'test', primary: primary_joy, secondary: secondary_high)
|
|
155
|
+
result = engine.to_h
|
|
156
|
+
expect(result).to have_key(:appraisals)
|
|
157
|
+
expect(result).to have_key(:coping_strategies)
|
|
158
|
+
expect(result).to have_key(:history_size)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Appraisal::Helpers::Appraisal do
|
|
4
|
+
let(:primary_low) { { relevance: 0.2, goal_congruence: 0.2, goal_importance: 0.5 } }
|
|
5
|
+
let(:primary_high) { { relevance: 0.8, goal_congruence: 0.8, goal_importance: 0.9 } }
|
|
6
|
+
let(:secondary_low) { { coping_potential: 0.2, control_expectation: 0.3, future_expectancy: 0.4 } }
|
|
7
|
+
let(:secondary_high) { { coping_potential: 0.8, control_expectation: 0.7, future_expectancy: 0.6 } }
|
|
8
|
+
|
|
9
|
+
def build(primary: primary_high, secondary: secondary_high, domain: 'work')
|
|
10
|
+
described_class.new(event: 'test event', primary: primary, secondary: secondary, domain: domain)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe '#initialize' do
|
|
14
|
+
it 'sets id, event, domain' do
|
|
15
|
+
appraisal = build
|
|
16
|
+
expect(appraisal.id).to be_a(String)
|
|
17
|
+
expect(appraisal.event).to eq('test event')
|
|
18
|
+
expect(appraisal.domain).to eq('work')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'normalizes primary dimensions' do
|
|
22
|
+
appraisal = build
|
|
23
|
+
expect(appraisal.primary.keys).to contain_exactly(:relevance, :goal_congruence, :goal_importance)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'normalizes secondary dimensions' do
|
|
27
|
+
appraisal = build
|
|
28
|
+
expect(appraisal.secondary.keys).to contain_exactly(:coping_potential, :control_expectation, :future_expectancy)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'clamps out-of-range values' do
|
|
32
|
+
appraisal = described_class.new(
|
|
33
|
+
event: 'e',
|
|
34
|
+
primary: { relevance: 2.0, goal_congruence: -0.5, goal_importance: 0.5 },
|
|
35
|
+
secondary: secondary_high
|
|
36
|
+
)
|
|
37
|
+
expect(appraisal.primary[:relevance]).to eq(1.0)
|
|
38
|
+
expect(appraisal.primary[:goal_congruence]).to eq(0.0)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'sets default intensity' do
|
|
42
|
+
expect(build.intensity).to eq(0.5)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'sets reappraised to false' do
|
|
46
|
+
expect(build.reappraised).to be(false)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#compute_emotion' do
|
|
51
|
+
it 'returns :indifference when relevance is low' do
|
|
52
|
+
appraisal = described_class.new(
|
|
53
|
+
event: 'e',
|
|
54
|
+
primary: { relevance: 0.2, goal_congruence: 0.5, goal_importance: 0.5 },
|
|
55
|
+
secondary: secondary_high
|
|
56
|
+
)
|
|
57
|
+
expect(appraisal.emotional_outcome).to eq(:indifference)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'returns :anxiety for threat with low coping' do
|
|
61
|
+
appraisal = described_class.new(
|
|
62
|
+
event: 'e',
|
|
63
|
+
primary: { relevance: 0.9, goal_congruence: 0.3, goal_importance: 0.8 },
|
|
64
|
+
secondary: { coping_potential: 0.2, control_expectation: 0.3, future_expectancy: 0.4 }
|
|
65
|
+
)
|
|
66
|
+
expect(appraisal.emotional_outcome).to eq(:anxiety)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'returns :challenge for threat with high coping' do
|
|
70
|
+
appraisal = described_class.new(
|
|
71
|
+
event: 'e',
|
|
72
|
+
primary: { relevance: 0.9, goal_congruence: 0.3, goal_importance: 0.8 },
|
|
73
|
+
secondary: { coping_potential: 0.8, control_expectation: 0.7, future_expectancy: 0.6 }
|
|
74
|
+
)
|
|
75
|
+
expect(appraisal.emotional_outcome).to eq(:challenge)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'returns :anger for very low goal_congruence' do
|
|
79
|
+
appraisal = described_class.new(
|
|
80
|
+
event: 'e',
|
|
81
|
+
primary: { relevance: 0.9, goal_congruence: 0.2, goal_importance: 0.8 },
|
|
82
|
+
secondary: { coping_potential: 0.5, control_expectation: 0.5, future_expectancy: 0.5 }
|
|
83
|
+
)
|
|
84
|
+
expect(appraisal.emotional_outcome).to eq(:anger)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'returns :joy for high goal_congruence' do
|
|
88
|
+
appraisal = described_class.new(
|
|
89
|
+
event: 'e',
|
|
90
|
+
primary: { relevance: 0.9, goal_congruence: 0.9, goal_importance: 0.8 },
|
|
91
|
+
secondary: secondary_high
|
|
92
|
+
)
|
|
93
|
+
expect(appraisal.emotional_outcome).to eq(:joy)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'returns :sadness as fallback' do
|
|
97
|
+
appraisal = described_class.new(
|
|
98
|
+
event: 'e',
|
|
99
|
+
primary: { relevance: 0.9, goal_congruence: 0.5, goal_importance: 0.5 },
|
|
100
|
+
secondary: { coping_potential: 0.5, control_expectation: 0.5, future_expectancy: 0.5 }
|
|
101
|
+
)
|
|
102
|
+
expect(appraisal.emotional_outcome).to eq(:sadness)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
describe '#reappraise' do
|
|
107
|
+
it 'updates emotion and reduces intensity' do
|
|
108
|
+
appraisal = described_class.new(
|
|
109
|
+
event: 'e', primary: primary_low, secondary: secondary_low
|
|
110
|
+
)
|
|
111
|
+
original_intensity = appraisal.intensity
|
|
112
|
+
appraisal.reappraise(new_primary: primary_high, new_secondary: secondary_high)
|
|
113
|
+
expect(appraisal.reappraised).to be(true)
|
|
114
|
+
expect(appraisal.intensity).to be < original_intensity
|
|
115
|
+
expect(appraisal.reappraised_at).not_to be_nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'recomputes emotional_outcome' do
|
|
119
|
+
appraisal = described_class.new(
|
|
120
|
+
event: 'e',
|
|
121
|
+
primary: { relevance: 0.2, goal_congruence: 0.5, goal_importance: 0.5 },
|
|
122
|
+
secondary: secondary_high
|
|
123
|
+
)
|
|
124
|
+
expect(appraisal.emotional_outcome).to eq(:indifference)
|
|
125
|
+
appraisal.reappraise(
|
|
126
|
+
new_primary: { relevance: 0.9, goal_congruence: 0.9, goal_importance: 0.8 },
|
|
127
|
+
new_secondary: secondary_high
|
|
128
|
+
)
|
|
129
|
+
expect(appraisal.emotional_outcome).to eq(:joy)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'applies REAPPRAISAL_DISCOUNT to intensity' do
|
|
133
|
+
appraisal = build
|
|
134
|
+
appraisal.reappraise(new_primary: primary_high, new_secondary: secondary_high)
|
|
135
|
+
expected = 0.5 * (1 - Legion::Extensions::Appraisal::Helpers::Constants::REAPPRAISAL_DISCOUNT)
|
|
136
|
+
expect(appraisal.intensity).to be_within(0.001).of(expected)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
describe '#decay!' do
|
|
141
|
+
it 'reduces intensity by DECAY_RATE' do
|
|
142
|
+
appraisal = build
|
|
143
|
+
appraisal.decay!
|
|
144
|
+
expected = 0.5 - Legion::Extensions::Appraisal::Helpers::Constants::DECAY_RATE
|
|
145
|
+
expect(appraisal.intensity).to be_within(0.001).of(expected)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'does not go below INTENSITY_FLOOR' do
|
|
149
|
+
appraisal = described_class.new(
|
|
150
|
+
event: 'e',
|
|
151
|
+
primary: { relevance: 0.9, goal_congruence: 0.9, goal_importance: 0.9 },
|
|
152
|
+
secondary: secondary_high
|
|
153
|
+
)
|
|
154
|
+
60.times { appraisal.decay! }
|
|
155
|
+
expect(appraisal.intensity).to eq(0.0)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
describe '#assign_coping' do
|
|
160
|
+
it 'sets coping_strategy' do
|
|
161
|
+
appraisal = build
|
|
162
|
+
appraisal.assign_coping('reframing')
|
|
163
|
+
expect(appraisal.coping_strategy).to eq('reframing')
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
describe '#to_h' do
|
|
168
|
+
it 'returns a hash with expected keys' do
|
|
169
|
+
appraisal = build
|
|
170
|
+
keys = appraisal.to_h.keys
|
|
171
|
+
expect(keys).to include(:id, :event, :domain, :primary, :secondary, :emotional_outcome,
|
|
172
|
+
:intensity, :coping_strategy, :reappraised, :created_at, :reappraised_at)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Appraisal::Helpers::Constants do
|
|
4
|
+
let(:mod) { described_class }
|
|
5
|
+
|
|
6
|
+
it 'defines MAX_APPRAISALS' do
|
|
7
|
+
expect(mod::MAX_APPRAISALS).to eq(200)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it 'defines MAX_COPING_STRATEGIES' do
|
|
11
|
+
expect(mod::MAX_COPING_STRATEGIES).to eq(50)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'defines MAX_HISTORY' do
|
|
15
|
+
expect(mod::MAX_HISTORY).to eq(300)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'defines intensity bounds' do
|
|
19
|
+
expect(mod::INTENSITY_FLOOR).to eq(0.0)
|
|
20
|
+
expect(mod::INTENSITY_CEILING).to eq(1.0)
|
|
21
|
+
expect(mod::DEFAULT_INTENSITY).to eq(0.5)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'defines DECAY_RATE' do
|
|
25
|
+
expect(mod::DECAY_RATE).to eq(0.02)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'defines REAPPRAISAL_DISCOUNT' do
|
|
29
|
+
expect(mod::REAPPRAISAL_DISCOUNT).to eq(0.3)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'defines PRIMARY_DIMENSIONS' do
|
|
33
|
+
expect(mod::PRIMARY_DIMENSIONS).to contain_exactly(:relevance, :goal_congruence, :goal_importance)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'defines SECONDARY_DIMENSIONS' do
|
|
37
|
+
expect(mod::SECONDARY_DIMENSIONS).to contain_exactly(:coping_potential, :control_expectation, :future_expectancy)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'maps APPRAISAL_EMOTIONS' do
|
|
41
|
+
expect(mod::APPRAISAL_EMOTIONS[:threat_low_coping]).to eq(:anxiety)
|
|
42
|
+
expect(mod::APPRAISAL_EMOTIONS[:goal_congruent]).to eq(:joy)
|
|
43
|
+
expect(mod::APPRAISAL_EMOTIONS[:irrelevant]).to eq(:indifference)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'defines COPING_TYPES' do
|
|
47
|
+
expect(mod::COPING_TYPES).to include(:problem_focused, :emotion_focused, :meaning_focused)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/appraisal/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Appraisal::Runners::Appraisal do
|
|
6
|
+
let(:client) { Legion::Extensions::Appraisal::Client.new }
|
|
7
|
+
|
|
8
|
+
let(:primary_joy) { { relevance: 0.9, goal_congruence: 0.9, goal_importance: 0.8 } }
|
|
9
|
+
let(:primary_threat) { { relevance: 0.9, goal_congruence: 0.3, goal_importance: 0.8 } }
|
|
10
|
+
let(:secondary_low) { { coping_potential: 0.2, control_expectation: 0.3, future_expectancy: 0.4 } }
|
|
11
|
+
let(:secondary_high) { { coping_potential: 0.8, control_expectation: 0.7, future_expectancy: 0.6 } }
|
|
12
|
+
|
|
13
|
+
describe '#appraise_event' do
|
|
14
|
+
it 'returns success: true with an appraisal' do
|
|
15
|
+
result = client.appraise_event(event: 'deadline', primary: primary_joy, secondary: secondary_high)
|
|
16
|
+
expect(result[:success]).to be(true)
|
|
17
|
+
expect(result[:appraisal]).to include(:id, :emotional_outcome)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'assigns emotional_outcome based on appraisal pattern' do
|
|
21
|
+
result = client.appraise_event(event: 'win', primary: primary_joy, secondary: secondary_high)
|
|
22
|
+
expect(result[:appraisal][:emotional_outcome]).to eq(:joy)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'accepts optional domain' do
|
|
26
|
+
result = client.appraise_event(event: 'test', primary: primary_joy, secondary: secondary_high,
|
|
27
|
+
domain: 'work')
|
|
28
|
+
expect(result[:appraisal][:domain]).to eq('work')
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe '#reappraise_event' do
|
|
33
|
+
it 'updates existing appraisal' do
|
|
34
|
+
appraisal_id = client.appraise_event(event: 'e', primary: primary_threat,
|
|
35
|
+
secondary: secondary_low)[:appraisal][:id]
|
|
36
|
+
result = client.reappraise_event(appraisal_id: appraisal_id, new_primary: primary_joy,
|
|
37
|
+
new_secondary: secondary_high)
|
|
38
|
+
expect(result[:success]).to be(true)
|
|
39
|
+
expect(result[:appraisal][:reappraised]).to be(true)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'returns failure for unknown id' do
|
|
43
|
+
result = client.reappraise_event(appraisal_id: 'unknown', new_primary: primary_joy,
|
|
44
|
+
new_secondary: secondary_high)
|
|
45
|
+
expect(result[:success]).to be(false)
|
|
46
|
+
expect(result[:error]).to include('not found')
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#select_coping_strategy' do
|
|
51
|
+
it 'assigns a coping strategy to the appraisal' do
|
|
52
|
+
appraisal_id = client.appraise_event(event: 'e', primary: primary_threat,
|
|
53
|
+
secondary: secondary_low)[:appraisal][:id]
|
|
54
|
+
result = client.select_coping_strategy(appraisal_id: appraisal_id, coping_type: :problem_focused)
|
|
55
|
+
expect(result[:success]).to be(true)
|
|
56
|
+
expect(result[:appraisal][:coping_strategy]).not_to be_nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'returns failure for unknown appraisal' do
|
|
60
|
+
result = client.select_coping_strategy(appraisal_id: 'unknown', coping_type: :problem_focused)
|
|
61
|
+
expect(result[:success]).to be(false)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe '#add_coping_strategy' do
|
|
66
|
+
it 'registers a strategy' do
|
|
67
|
+
result = client.add_coping_strategy(name: 'journaling', coping_type: :emotion_focused,
|
|
68
|
+
effectiveness: 0.75)
|
|
69
|
+
expect(result[:success]).to be(true)
|
|
70
|
+
expect(result[:name]).to eq('journaling')
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe '#evaluate_coping' do
|
|
75
|
+
it 'returns effectiveness for an appraisal with coping' do
|
|
76
|
+
client.add_coping_strategy(name: 'breathing', coping_type: :emotion_focused, effectiveness: 0.8)
|
|
77
|
+
appraisal_id = client.appraise_event(event: 'e', primary: primary_threat,
|
|
78
|
+
secondary: secondary_low)[:appraisal][:id]
|
|
79
|
+
client.select_coping_strategy(appraisal_id: appraisal_id, coping_type: :emotion_focused)
|
|
80
|
+
result = client.evaluate_coping(appraisal_id: appraisal_id)
|
|
81
|
+
expect(result[:success]).to be(true)
|
|
82
|
+
expect(result[:effectiveness]).to be_a(Float)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
describe '#emotional_pattern' do
|
|
87
|
+
it 'returns success with pattern hash' do
|
|
88
|
+
client.appraise_event(event: 'a', primary: primary_joy, secondary: secondary_high)
|
|
89
|
+
client.appraise_event(event: 'b', primary: primary_joy, secondary: secondary_high)
|
|
90
|
+
result = client.emotional_pattern
|
|
91
|
+
expect(result[:success]).to be(true)
|
|
92
|
+
expect(result[:pattern]).to be_a(Hash)
|
|
93
|
+
expect(result[:pattern][:joy]).to eq(2)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe '#update_appraisal' do
|
|
98
|
+
it 'runs decay and returns success' do
|
|
99
|
+
client.appraise_event(event: 'e', primary: primary_joy, secondary: secondary_high)
|
|
100
|
+
result = client.update_appraisal
|
|
101
|
+
expect(result[:success]).to be(true)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
describe '#appraisal_stats' do
|
|
106
|
+
it 'returns stats hash with totals' do
|
|
107
|
+
client.appraise_event(event: 'a', primary: primary_joy, secondary: secondary_high)
|
|
108
|
+
client.appraise_event(event: 'b', primary: primary_threat, secondary: secondary_low)
|
|
109
|
+
result = client.appraisal_stats
|
|
110
|
+
expect(result[:success]).to be(true)
|
|
111
|
+
expect(result[:total]).to eq(2)
|
|
112
|
+
expect(result[:unresolved]).to eq(2)
|
|
113
|
+
expect(result[:history_size]).to be >= 2
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Logging
|
|
7
|
+
def self.debug(_msg); end
|
|
8
|
+
def self.info(_msg); end
|
|
9
|
+
def self.warn(_msg); end
|
|
10
|
+
def self.error(_msg); end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module Extensions
|
|
14
|
+
module Core; end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
require 'legion/extensions/appraisal'
|
|
19
|
+
|
|
20
|
+
RSpec.configure do |config|
|
|
21
|
+
config.example_status_persistence_file_path = '.rspec_status'
|
|
22
|
+
config.disable_monkey_patching!
|
|
23
|
+
config.expect_with(:rspec) { |c| c.syntax = :expect }
|
|
24
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-appraisal
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Esity
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: legion-gaia
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
description: Lazarus's Cognitive Appraisal Theory for brain-modeled agentic AI
|
|
27
|
+
email:
|
|
28
|
+
- matthewdiverson@gmail.com
|
|
29
|
+
executables: []
|
|
30
|
+
extensions: []
|
|
31
|
+
extra_rdoc_files: []
|
|
32
|
+
files:
|
|
33
|
+
- Gemfile
|
|
34
|
+
- LICENSE
|
|
35
|
+
- README.md
|
|
36
|
+
- lex-appraisal.gemspec
|
|
37
|
+
- lib/legion/extensions/appraisal.rb
|
|
38
|
+
- lib/legion/extensions/appraisal/client.rb
|
|
39
|
+
- lib/legion/extensions/appraisal/helpers/appraisal.rb
|
|
40
|
+
- lib/legion/extensions/appraisal/helpers/appraisal_engine.rb
|
|
41
|
+
- lib/legion/extensions/appraisal/helpers/constants.rb
|
|
42
|
+
- lib/legion/extensions/appraisal/runners/appraisal.rb
|
|
43
|
+
- lib/legion/extensions/appraisal/version.rb
|
|
44
|
+
- spec/legion/extensions/appraisal/client_spec.rb
|
|
45
|
+
- spec/legion/extensions/appraisal/helpers/appraisal_engine_spec.rb
|
|
46
|
+
- spec/legion/extensions/appraisal/helpers/appraisal_spec.rb
|
|
47
|
+
- spec/legion/extensions/appraisal/helpers/constants_spec.rb
|
|
48
|
+
- spec/legion/extensions/appraisal/runners/appraisal_spec.rb
|
|
49
|
+
- spec/spec_helper.rb
|
|
50
|
+
homepage: https://github.com/LegionIO/lex-appraisal
|
|
51
|
+
licenses:
|
|
52
|
+
- MIT
|
|
53
|
+
metadata:
|
|
54
|
+
homepage_uri: https://github.com/LegionIO/lex-appraisal
|
|
55
|
+
source_code_uri: https://github.com/LegionIO/lex-appraisal
|
|
56
|
+
documentation_uri: https://github.com/LegionIO/lex-appraisal
|
|
57
|
+
changelog_uri: https://github.com/LegionIO/lex-appraisal
|
|
58
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-appraisal/issues
|
|
59
|
+
rubygems_mfa_required: 'true'
|
|
60
|
+
rdoc_options: []
|
|
61
|
+
require_paths:
|
|
62
|
+
- lib
|
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.4'
|
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '0'
|
|
73
|
+
requirements: []
|
|
74
|
+
rubygems_version: 3.6.9
|
|
75
|
+
specification_version: 4
|
|
76
|
+
summary: LEX Appraisal
|
|
77
|
+
test_files: []
|