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 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75', require: false
9
+ gem 'rubocop-rspec', require: false
10
+
11
+ gem 'legion-gaia', path: '../../legion-gaia'
@@ -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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module MentalSimulation
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ 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