lex-mental-simulation 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 +11 -0
- data/lex-mental-simulation.gemspec +29 -0
- data/lib/legion/extensions/mental_simulation/helpers/client.rb +19 -0
- data/lib/legion/extensions/mental_simulation/helpers/constants.rb +39 -0
- data/lib/legion/extensions/mental_simulation/helpers/simulation.rb +112 -0
- data/lib/legion/extensions/mental_simulation/helpers/simulation_engine.rb +137 -0
- data/lib/legion/extensions/mental_simulation/helpers/simulation_step.rb +49 -0
- data/lib/legion/extensions/mental_simulation/runners/mental_simulation.rb +88 -0
- data/lib/legion/extensions/mental_simulation/version.rb +9 -0
- data/lib/legion/extensions/mental_simulation.rb +17 -0
- data/spec/legion/extensions/mental_simulation/helpers/constants_spec.rb +107 -0
- data/spec/legion/extensions/mental_simulation/helpers/simulation_engine_spec.rb +217 -0
- data/spec/legion/extensions/mental_simulation/helpers/simulation_spec.rb +223 -0
- data/spec/legion/extensions/mental_simulation/helpers/simulation_step_spec.rb +111 -0
- data/spec/legion/extensions/mental_simulation/runners/mental_simulation_spec.rb +184 -0
- data/spec/spec_helper.rb +20 -0
- metadata +77 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 062aab32a8aca9c6d7c1bc81ee6384e7be863287e5527498f92b53ba13dcfa56
|
|
4
|
+
data.tar.gz: 2ca21d491f8b5e39119d673384cbeeb18224bb153196d21a809ca6fe5c58a793
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3dfeea7fa9ab1a3aadea8dc3e360b44425882140adae59105850133031e12647b8503278e4379b4d90e8f48b0fd3798fe2ff7abede739362060b1a5cff6205d0
|
|
7
|
+
data.tar.gz: '049a502d61a973870ae10a0a5cef15dd258ae9103cdfc05f620bb7e2f898f6d3666b38405cbd769a2c807146588498d26a7348f2de21f1fdee60ccdf9256f737'
|
data/Gemfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/mental_simulation/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-mental-simulation'
|
|
7
|
+
spec.version = Legion::Extensions::MentalSimulation::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Mental Simulation'
|
|
12
|
+
spec.description = 'Forward mental simulation of action sequences — imagine a plan, predict step outcomes, evaluate before executing'
|
|
13
|
+
spec.homepage = 'https://github.com/LegionIO/lex-mental-simulation'
|
|
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-mental-simulation'
|
|
19
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-mental-simulation'
|
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-mental-simulation'
|
|
21
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-mental-simulation/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-mental-simulation.gemspec Gemfile]
|
|
26
|
+
end
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
spec.add_development_dependency 'legion-gaia'
|
|
29
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MentalSimulation
|
|
6
|
+
class Client
|
|
7
|
+
include Runners::MentalSimulation
|
|
8
|
+
|
|
9
|
+
def initialize(**)
|
|
10
|
+
@engine = Helpers::SimulationEngine.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
attr_reader :engine
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MentalSimulation
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
SIMULATION_STATES = %i[pending running completed failed aborted].freeze
|
|
9
|
+
STEP_OUTCOMES = %i[success partial_success failure unknown].freeze
|
|
10
|
+
|
|
11
|
+
CONFIDENCE_LABELS = {
|
|
12
|
+
(0.8..) => :very_confident,
|
|
13
|
+
(0.6...0.8) => :confident,
|
|
14
|
+
(0.4...0.6) => :uncertain,
|
|
15
|
+
(0.2...0.4) => :doubtful,
|
|
16
|
+
(..0.2) => :very_doubtful
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
RISK_LABELS = {
|
|
20
|
+
(0.8..) => :critical,
|
|
21
|
+
(0.6...0.8) => :high,
|
|
22
|
+
(0.4...0.6) => :moderate,
|
|
23
|
+
(0.2...0.4) => :low,
|
|
24
|
+
(..0.2) => :negligible
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
MAX_SIMULATIONS = 100
|
|
28
|
+
MAX_STEPS_PER_SIM = 50
|
|
29
|
+
MAX_HISTORY = 500
|
|
30
|
+
|
|
31
|
+
DEFAULT_CONFIDENCE = 0.5
|
|
32
|
+
CONFIDENCE_BOOST = 0.1
|
|
33
|
+
CONFIDENCE_PENALTY = 0.15
|
|
34
|
+
RISK_ACCUMULATION_RATE = 0.1
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module MentalSimulation
|
|
8
|
+
module Helpers
|
|
9
|
+
class Simulation
|
|
10
|
+
include Constants
|
|
11
|
+
|
|
12
|
+
attr_reader :id, :label, :domain, :steps, :state, :created_at, :completed_at
|
|
13
|
+
|
|
14
|
+
def initialize(label:, domain:)
|
|
15
|
+
@id = SecureRandom.uuid
|
|
16
|
+
@label = label
|
|
17
|
+
@domain = domain.to_sym
|
|
18
|
+
@steps = []
|
|
19
|
+
@state = :pending
|
|
20
|
+
@created_at = Time.now.utc
|
|
21
|
+
@completed_at = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def add_step(action:, predicted_outcome: :success, confidence: 0.5, risk: 0.1,
|
|
25
|
+
preconditions: [], postconditions: [])
|
|
26
|
+
step = SimulationStep.new(
|
|
27
|
+
action: action,
|
|
28
|
+
predicted_outcome: predicted_outcome,
|
|
29
|
+
confidence: confidence,
|
|
30
|
+
risk: risk,
|
|
31
|
+
preconditions: preconditions,
|
|
32
|
+
postconditions: postconditions
|
|
33
|
+
)
|
|
34
|
+
@steps << step
|
|
35
|
+
step
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def run!
|
|
39
|
+
@state = :running
|
|
40
|
+
accumulated_risk = 0.0
|
|
41
|
+
|
|
42
|
+
@steps.each do |step|
|
|
43
|
+
accumulated_risk += step.risk * RISK_ACCUMULATION_RATE
|
|
44
|
+
|
|
45
|
+
next unless step.predicted_outcome == :failure && step.confidence > 0.7
|
|
46
|
+
|
|
47
|
+
@state = :aborted
|
|
48
|
+
return self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
final_state = overall_confidence >= 0.5 && cumulative_risk < 0.6 ? :completed : :failed
|
|
52
|
+
@state = final_state
|
|
53
|
+
@completed_at = Time.now.utc
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def abort!
|
|
58
|
+
@state = :aborted
|
|
59
|
+
@completed_at = Time.now.utc
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def overall_confidence
|
|
64
|
+
return 0.0 if @steps.empty?
|
|
65
|
+
|
|
66
|
+
@steps.reduce(1.0) { |prod, step| prod * step.confidence }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def cumulative_risk
|
|
70
|
+
return 0.0 if @steps.empty?
|
|
71
|
+
|
|
72
|
+
1.0 - @steps.reduce(1.0) { |prod, step| prod * (1.0 - step.risk) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def favorable?
|
|
76
|
+
@state == :completed && overall_confidence >= 0.5 && cumulative_risk < 0.6
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def confidence_label
|
|
80
|
+
CONFIDENCE_LABELS.find { |range, _| range.cover?(overall_confidence) }&.last || :very_doubtful
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def risk_label
|
|
84
|
+
RISK_LABELS.find { |range, _| range.cover?(cumulative_risk) }&.last || :negligible
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def step_count
|
|
88
|
+
@steps.size
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def to_h
|
|
92
|
+
{
|
|
93
|
+
id: @id,
|
|
94
|
+
label: @label,
|
|
95
|
+
domain: @domain,
|
|
96
|
+
state: @state,
|
|
97
|
+
step_count: step_count,
|
|
98
|
+
overall_confidence: overall_confidence,
|
|
99
|
+
cumulative_risk: cumulative_risk,
|
|
100
|
+
confidence_label: confidence_label,
|
|
101
|
+
risk_label: risk_label,
|
|
102
|
+
favorable: favorable?,
|
|
103
|
+
steps: @steps.map(&:to_h),
|
|
104
|
+
created_at: @created_at,
|
|
105
|
+
completed_at: @completed_at
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MentalSimulation
|
|
6
|
+
module Helpers
|
|
7
|
+
class SimulationEngine
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@simulations = {}
|
|
12
|
+
@history = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create_simulation(label:, domain:)
|
|
16
|
+
prune_simulations if @simulations.size >= MAX_SIMULATIONS
|
|
17
|
+
sim = Simulation.new(label: label, domain: domain)
|
|
18
|
+
@simulations[sim.id] = sim
|
|
19
|
+
sim
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def add_simulation_step(simulation_id:, action:, predicted_outcome: :success,
|
|
23
|
+
confidence: 0.5, risk: 0.1, preconditions: [], postconditions: [])
|
|
24
|
+
sim = @simulations[simulation_id]
|
|
25
|
+
return { error: :simulation_not_found } unless sim
|
|
26
|
+
|
|
27
|
+
return { error: :max_steps_reached, max: MAX_STEPS_PER_SIM } if sim.step_count >= MAX_STEPS_PER_SIM
|
|
28
|
+
|
|
29
|
+
step = sim.add_step(
|
|
30
|
+
action: action,
|
|
31
|
+
predicted_outcome: predicted_outcome,
|
|
32
|
+
confidence: confidence,
|
|
33
|
+
risk: risk,
|
|
34
|
+
preconditions: preconditions,
|
|
35
|
+
postconditions: postconditions
|
|
36
|
+
)
|
|
37
|
+
{ added: true, step_id: step.id, step_count: sim.step_count }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def run_simulation(simulation_id:)
|
|
41
|
+
sim = @simulations[simulation_id]
|
|
42
|
+
return { error: :simulation_not_found } unless sim
|
|
43
|
+
|
|
44
|
+
sim.run!
|
|
45
|
+
archive_simulation(sim)
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
simulation_id: sim.id,
|
|
49
|
+
state: sim.state,
|
|
50
|
+
overall_confidence: sim.overall_confidence,
|
|
51
|
+
cumulative_risk: sim.cumulative_risk,
|
|
52
|
+
confidence_label: sim.confidence_label,
|
|
53
|
+
risk_label: sim.risk_label,
|
|
54
|
+
favorable: sim.favorable?,
|
|
55
|
+
step_count: sim.step_count
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def abort_simulation(simulation_id:)
|
|
60
|
+
sim = @simulations[simulation_id]
|
|
61
|
+
return { error: :simulation_not_found } unless sim
|
|
62
|
+
|
|
63
|
+
sim.abort!
|
|
64
|
+
{ simulation_id: sim.id, state: sim.state, aborted: true }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def assess_simulation(simulation_id:)
|
|
68
|
+
sim = @simulations[simulation_id]
|
|
69
|
+
return { error: :simulation_not_found } unless sim
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
simulation_id: sim.id,
|
|
73
|
+
label: sim.label,
|
|
74
|
+
domain: sim.domain,
|
|
75
|
+
state: sim.state,
|
|
76
|
+
step_count: sim.step_count,
|
|
77
|
+
overall_confidence: sim.overall_confidence,
|
|
78
|
+
cumulative_risk: sim.cumulative_risk,
|
|
79
|
+
confidence_label: sim.confidence_label,
|
|
80
|
+
risk_label: sim.risk_label,
|
|
81
|
+
favorable: sim.favorable?,
|
|
82
|
+
steps: sim.steps.map(&:to_h)
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def favorable_simulations
|
|
87
|
+
@simulations.values.select(&:favorable?)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def failed_simulations
|
|
91
|
+
@simulations.values.select { |s| s.state == :failed }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def simulations_by_domain(domain:)
|
|
95
|
+
sym = domain.to_sym
|
|
96
|
+
@simulations.values.select { |s| s.domain == sym }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def riskiest_simulations(limit: 5)
|
|
100
|
+
@simulations.values
|
|
101
|
+
.sort_by { |s| -s.cumulative_risk }
|
|
102
|
+
.first(limit)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def most_confident(limit: 5)
|
|
106
|
+
@simulations.values
|
|
107
|
+
.sort_by { |s| -s.overall_confidence }
|
|
108
|
+
.first(limit)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def to_h
|
|
112
|
+
{
|
|
113
|
+
total_simulations: @simulations.size,
|
|
114
|
+
history_size: @history.size,
|
|
115
|
+
favorable_count: favorable_simulations.size,
|
|
116
|
+
failed_count: failed_simulations.size,
|
|
117
|
+
simulations: @simulations.values.map(&:to_h)
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def archive_simulation(sim)
|
|
124
|
+
@history << { simulation_id: sim.id, state: sim.state, archived_at: Time.now.utc }
|
|
125
|
+
@history.shift while @history.size > MAX_HISTORY
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def prune_simulations
|
|
129
|
+
completed = @simulations.select { |_, s| %i[completed failed aborted].include?(s.state) }
|
|
130
|
+
oldest = completed.values.min_by(&:created_at)
|
|
131
|
+
@simulations.delete(oldest.id) if oldest
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module MentalSimulation
|
|
8
|
+
module Helpers
|
|
9
|
+
class SimulationStep
|
|
10
|
+
attr_reader :id, :action, :predicted_outcome, :confidence, :risk,
|
|
11
|
+
:preconditions, :postconditions, :created_at
|
|
12
|
+
|
|
13
|
+
def initialize(action:, predicted_outcome: :success, confidence: 0.5, risk: 0.1,
|
|
14
|
+
preconditions: [], postconditions: [])
|
|
15
|
+
@id = SecureRandom.uuid
|
|
16
|
+
@action = action
|
|
17
|
+
@predicted_outcome = predicted_outcome
|
|
18
|
+
@confidence = confidence.clamp(0.0, 1.0)
|
|
19
|
+
@risk = risk.clamp(0.0, 1.0)
|
|
20
|
+
@preconditions = Array(preconditions)
|
|
21
|
+
@postconditions = Array(postconditions)
|
|
22
|
+
@created_at = Time.now.utc
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def favorable?
|
|
26
|
+
%i[success partial_success].include?(@predicted_outcome) && @confidence >= 0.5
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def risky?
|
|
30
|
+
@risk >= 0.6
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_h
|
|
34
|
+
{
|
|
35
|
+
id: @id,
|
|
36
|
+
action: @action,
|
|
37
|
+
predicted_outcome: @predicted_outcome,
|
|
38
|
+
confidence: @confidence,
|
|
39
|
+
risk: @risk,
|
|
40
|
+
preconditions: @preconditions,
|
|
41
|
+
postconditions: @postconditions,
|
|
42
|
+
created_at: @created_at
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MentalSimulation
|
|
6
|
+
module Runners
|
|
7
|
+
module MentalSimulation
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def create_mental_simulation(label:, domain:, **)
|
|
12
|
+
sim = engine.create_simulation(label: label, domain: domain)
|
|
13
|
+
Legion::Logging.debug "[mental_simulation] created simulation id=#{sim.id[0..7]} label=#{label} domain=#{domain}"
|
|
14
|
+
{ simulation_id: sim.id, label: sim.label, domain: sim.domain, state: sim.state }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add_simulation_step(simulation_id:, action:, predicted_outcome: :success,
|
|
18
|
+
confidence: 0.5, risk: 0.1, preconditions: [], postconditions: [], **)
|
|
19
|
+
result = engine.add_simulation_step(
|
|
20
|
+
simulation_id: simulation_id,
|
|
21
|
+
action: action,
|
|
22
|
+
predicted_outcome: predicted_outcome,
|
|
23
|
+
confidence: confidence,
|
|
24
|
+
risk: risk,
|
|
25
|
+
preconditions: preconditions,
|
|
26
|
+
postconditions: postconditions
|
|
27
|
+
)
|
|
28
|
+
Legion::Logging.debug "[mental_simulation] add_step sim=#{simulation_id[0..7]} action=#{action} result=#{result[:added]}"
|
|
29
|
+
result
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def run_mental_simulation(simulation_id:, **)
|
|
33
|
+
result = engine.run_simulation(simulation_id: simulation_id)
|
|
34
|
+
if result[:error]
|
|
35
|
+
Legion::Logging.warn "[mental_simulation] run failed: #{result[:error]}"
|
|
36
|
+
else
|
|
37
|
+
Legion::Logging.info "[mental_simulation] ran sim=#{simulation_id[0..7]} " \
|
|
38
|
+
"state=#{result[:state]} favorable=#{result[:favorable]}"
|
|
39
|
+
end
|
|
40
|
+
result
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def abort_mental_simulation(simulation_id:, **)
|
|
44
|
+
result = engine.abort_simulation(simulation_id: simulation_id)
|
|
45
|
+
Legion::Logging.info "[mental_simulation] aborted sim=#{simulation_id[0..7]}"
|
|
46
|
+
result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def assess_mental_simulation(simulation_id:, **)
|
|
50
|
+
result = engine.assess_simulation(simulation_id: simulation_id)
|
|
51
|
+
Legion::Logging.debug "[mental_simulation] assessed sim=#{simulation_id[0..7]} steps=#{result[:step_count]}"
|
|
52
|
+
result
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def favorable_simulations_report(**)
|
|
56
|
+
sims = engine.favorable_simulations
|
|
57
|
+
Legion::Logging.debug "[mental_simulation] favorable count=#{sims.size}"
|
|
58
|
+
{ simulations: sims.map(&:to_h), count: sims.size }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def failed_simulations_report(**)
|
|
62
|
+
sims = engine.failed_simulations
|
|
63
|
+
Legion::Logging.debug "[mental_simulation] failed count=#{sims.size}"
|
|
64
|
+
{ simulations: sims.map(&:to_h), count: sims.size }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def riskiest_simulations_report(limit: 5, **)
|
|
68
|
+
sims = engine.riskiest_simulations(limit: limit)
|
|
69
|
+
Legion::Logging.debug "[mental_simulation] riskiest count=#{sims.size} limit=#{limit}"
|
|
70
|
+
{ simulations: sims.map(&:to_h), count: sims.size }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def mental_simulation_stats(**)
|
|
74
|
+
stats = engine.to_h.except(:simulations)
|
|
75
|
+
Legion::Logging.debug "[mental_simulation] stats total=#{stats[:total_simulations]}"
|
|
76
|
+
stats
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def engine
|
|
82
|
+
@engine ||= Helpers::SimulationEngine.new
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/mental_simulation/version'
|
|
4
|
+
require 'legion/extensions/mental_simulation/helpers/constants'
|
|
5
|
+
require 'legion/extensions/mental_simulation/helpers/simulation_step'
|
|
6
|
+
require 'legion/extensions/mental_simulation/helpers/simulation'
|
|
7
|
+
require 'legion/extensions/mental_simulation/helpers/simulation_engine'
|
|
8
|
+
require 'legion/extensions/mental_simulation/runners/mental_simulation'
|
|
9
|
+
require 'legion/extensions/mental_simulation/helpers/client'
|
|
10
|
+
|
|
11
|
+
module Legion
|
|
12
|
+
module Extensions
|
|
13
|
+
module MentalSimulation
|
|
14
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::MentalSimulation::Helpers::Constants do
|
|
4
|
+
describe 'SIMULATION_STATES' do
|
|
5
|
+
it 'is frozen' do
|
|
6
|
+
expect(described_class::SIMULATION_STATES).to be_frozen
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it 'contains expected states' do
|
|
10
|
+
expect(described_class::SIMULATION_STATES).to include(:pending, :running, :completed, :failed, :aborted)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe 'STEP_OUTCOMES' do
|
|
15
|
+
it 'is frozen' do
|
|
16
|
+
expect(described_class::STEP_OUTCOMES).to be_frozen
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'contains expected outcomes' do
|
|
20
|
+
expect(described_class::STEP_OUTCOMES).to include(:success, :partial_success, :failure, :unknown)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe 'CONFIDENCE_LABELS' do
|
|
25
|
+
it 'labels 0.9 as very_confident' do
|
|
26
|
+
match = described_class::CONFIDENCE_LABELS.find { |r, _| r.cover?(0.9) }&.last
|
|
27
|
+
expect(match).to eq(:very_confident)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'labels 0.7 as confident' do
|
|
31
|
+
match = described_class::CONFIDENCE_LABELS.find { |r, _| r.cover?(0.7) }&.last
|
|
32
|
+
expect(match).to eq(:confident)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'labels 0.5 as uncertain' do
|
|
36
|
+
match = described_class::CONFIDENCE_LABELS.find { |r, _| r.cover?(0.5) }&.last
|
|
37
|
+
expect(match).to eq(:uncertain)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'labels 0.3 as doubtful' do
|
|
41
|
+
match = described_class::CONFIDENCE_LABELS.find { |r, _| r.cover?(0.3) }&.last
|
|
42
|
+
expect(match).to eq(:doubtful)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'labels 0.1 as very_doubtful' do
|
|
46
|
+
match = described_class::CONFIDENCE_LABELS.find { |r, _| r.cover?(0.1) }&.last
|
|
47
|
+
expect(match).to eq(:very_doubtful)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe 'RISK_LABELS' do
|
|
52
|
+
it 'labels 0.9 as critical' do
|
|
53
|
+
match = described_class::RISK_LABELS.find { |r, _| r.cover?(0.9) }&.last
|
|
54
|
+
expect(match).to eq(:critical)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'labels 0.7 as high' do
|
|
58
|
+
match = described_class::RISK_LABELS.find { |r, _| r.cover?(0.7) }&.last
|
|
59
|
+
expect(match).to eq(:high)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'labels 0.5 as moderate' do
|
|
63
|
+
match = described_class::RISK_LABELS.find { |r, _| r.cover?(0.5) }&.last
|
|
64
|
+
expect(match).to eq(:moderate)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'labels 0.3 as low' do
|
|
68
|
+
match = described_class::RISK_LABELS.find { |r, _| r.cover?(0.3) }&.last
|
|
69
|
+
expect(match).to eq(:low)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'labels 0.1 as negligible' do
|
|
73
|
+
match = described_class::RISK_LABELS.find { |r, _| r.cover?(0.1) }&.last
|
|
74
|
+
expect(match).to eq(:negligible)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe 'numeric constants' do
|
|
79
|
+
it 'MAX_SIMULATIONS is 100' do
|
|
80
|
+
expect(described_class::MAX_SIMULATIONS).to eq(100)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'MAX_STEPS_PER_SIM is 50' do
|
|
84
|
+
expect(described_class::MAX_STEPS_PER_SIM).to eq(50)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'MAX_HISTORY is 500' do
|
|
88
|
+
expect(described_class::MAX_HISTORY).to eq(500)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'DEFAULT_CONFIDENCE is 0.5' do
|
|
92
|
+
expect(described_class::DEFAULT_CONFIDENCE).to eq(0.5)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'CONFIDENCE_BOOST is 0.1' do
|
|
96
|
+
expect(described_class::CONFIDENCE_BOOST).to eq(0.1)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'CONFIDENCE_PENALTY is 0.15' do
|
|
100
|
+
expect(described_class::CONFIDENCE_PENALTY).to eq(0.15)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'RISK_ACCUMULATION_RATE is 0.1' do
|
|
104
|
+
expect(described_class::RISK_ACCUMULATION_RATE).to eq(0.1)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|