lex-prospective-memory 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-prospective-memory.gemspec +29 -0
- data/lib/legion/extensions/prospective_memory/client.rb +24 -0
- data/lib/legion/extensions/prospective_memory/helpers/constants.rb +28 -0
- data/lib/legion/extensions/prospective_memory/helpers/intention.rb +90 -0
- data/lib/legion/extensions/prospective_memory/helpers/prospective_engine.rb +153 -0
- data/lib/legion/extensions/prospective_memory/runners/prospective_memory.rb +142 -0
- data/lib/legion/extensions/prospective_memory/version.rb +9 -0
- data/lib/legion/extensions/prospective_memory.rb +16 -0
- data/spec/legion/extensions/prospective_memory/client_spec.rb +24 -0
- data/spec/legion/extensions/prospective_memory/helpers/constants_spec.rb +84 -0
- data/spec/legion/extensions/prospective_memory/helpers/intention_spec.rb +193 -0
- data/spec/legion/extensions/prospective_memory/helpers/prospective_engine_spec.rb +255 -0
- data/spec/legion/extensions/prospective_memory/runners/prospective_memory_spec.rb +220 -0
- data/spec/spec_helper.rb +20 -0
- metadata +76 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6f9b96274e56e32c5e49088b65443551c071b613f3b299535d4456125ea924d9
|
|
4
|
+
data.tar.gz: 41224c4bc01e0b66da4d25ba71c3451150002099fe94ae4572d1855561c98d0d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 46f6902aeb852cf1733825242cd35347e2e5dcc2e8febdc83d123702171648a3b6474070fa755af91fc39be2f9f84aa7b301d6197e266b8d6450c095425d1ef1
|
|
7
|
+
data.tar.gz: 4fe8ce5afa4afec96fe2aab1475f8b3b3bd69c79d54b62f09f2d949ab2903555600a9a604cf016c6b01ec2c5e0cdd2b3c819259f838db66f0a2130cf5018e26a
|
data/Gemfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/prospective_memory/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-prospective-memory'
|
|
7
|
+
spec.version = Legion::Extensions::ProspectiveMemory::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Prospective Memory'
|
|
12
|
+
spec.description = 'Prospective memory engine: manages intentions, monitors trigger conditions, and executes when conditions are met'
|
|
13
|
+
spec.homepage = 'https://github.com/LegionIO/lex-prospective-memory'
|
|
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-prospective-memory'
|
|
19
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-prospective-memory'
|
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-prospective-memory'
|
|
21
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-prospective-memory/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-prospective-memory.gemspec Gemfile]
|
|
26
|
+
end
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
spec.add_development_dependency 'legion-gaia'
|
|
29
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/prospective_memory/helpers/constants'
|
|
4
|
+
require 'legion/extensions/prospective_memory/helpers/intention'
|
|
5
|
+
require 'legion/extensions/prospective_memory/helpers/prospective_engine'
|
|
6
|
+
require 'legion/extensions/prospective_memory/runners/prospective_memory'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Extensions
|
|
10
|
+
module ProspectiveMemory
|
|
11
|
+
class Client
|
|
12
|
+
include Runners::ProspectiveMemory
|
|
13
|
+
|
|
14
|
+
def initialize(**)
|
|
15
|
+
@prospective_engine = Helpers::ProspectiveEngine.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :prospective_engine
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module ProspectiveMemory
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
MAX_INTENTIONS = 300
|
|
9
|
+
DEFAULT_URGENCY = 0.5
|
|
10
|
+
URGENCY_DECAY = 0.01
|
|
11
|
+
URGENCY_BOOST = 0.1
|
|
12
|
+
CHECK_INTERVAL = 60
|
|
13
|
+
|
|
14
|
+
URGENCY_LABELS = {
|
|
15
|
+
(0.8..) => :critical,
|
|
16
|
+
(0.6...0.8) => :high,
|
|
17
|
+
(0.4...0.6) => :moderate,
|
|
18
|
+
(0.2...0.4) => :low,
|
|
19
|
+
(..0.2) => :deferred
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
STATUS_TYPES = %i[pending monitoring triggered executed expired cancelled].freeze
|
|
23
|
+
TRIGGER_TYPES = %i[time_based event_based context_based activity_based].freeze
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module ProspectiveMemory
|
|
8
|
+
module Helpers
|
|
9
|
+
class Intention
|
|
10
|
+
attr_reader :id, :description, :trigger_type, :trigger_condition,
|
|
11
|
+
:domain, :created_at, :triggered_at, :executed_at, :expires_at, :status
|
|
12
|
+
|
|
13
|
+
attr_accessor :urgency
|
|
14
|
+
|
|
15
|
+
def initialize(description:, trigger_type:, trigger_condition:, urgency: Constants::DEFAULT_URGENCY,
|
|
16
|
+
domain: nil, expires_at: nil)
|
|
17
|
+
@id = SecureRandom.uuid
|
|
18
|
+
@description = description
|
|
19
|
+
@trigger_type = trigger_type
|
|
20
|
+
@trigger_condition = trigger_condition
|
|
21
|
+
@urgency = urgency.clamp(0.0, 1.0)
|
|
22
|
+
@domain = domain
|
|
23
|
+
@status = :pending
|
|
24
|
+
@created_at = Time.now.utc
|
|
25
|
+
@triggered_at = nil
|
|
26
|
+
@executed_at = nil
|
|
27
|
+
@expires_at = expires_at
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def monitor!
|
|
31
|
+
@status = :monitoring
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def trigger!
|
|
35
|
+
@status = :triggered
|
|
36
|
+
@triggered_at = Time.now.utc
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def execute!
|
|
40
|
+
@status = :executed
|
|
41
|
+
@executed_at = Time.now.utc
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def expire!
|
|
45
|
+
@status = :expired
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def cancel!
|
|
49
|
+
@status = :cancelled
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def expired?
|
|
53
|
+
return false unless @expires_at
|
|
54
|
+
|
|
55
|
+
Time.now.utc > @expires_at
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def boost_urgency!(amount: Constants::URGENCY_BOOST)
|
|
59
|
+
@urgency = (@urgency + amount).clamp(0.0, 1.0).round(10)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def decay_urgency!
|
|
63
|
+
@urgency = (@urgency - Constants::URGENCY_DECAY).clamp(0.0, 1.0).round(10)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def urgency_label
|
|
67
|
+
Constants::URGENCY_LABELS.find { |range, _label| range.cover?(@urgency) }&.last || :deferred
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def to_h
|
|
71
|
+
{
|
|
72
|
+
id: @id,
|
|
73
|
+
description: @description,
|
|
74
|
+
trigger_type: @trigger_type,
|
|
75
|
+
trigger_condition: @trigger_condition,
|
|
76
|
+
urgency: @urgency,
|
|
77
|
+
urgency_label: urgency_label,
|
|
78
|
+
status: @status,
|
|
79
|
+
domain: @domain,
|
|
80
|
+
created_at: @created_at,
|
|
81
|
+
triggered_at: @triggered_at,
|
|
82
|
+
executed_at: @executed_at,
|
|
83
|
+
expires_at: @expires_at
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module ProspectiveMemory
|
|
6
|
+
module Helpers
|
|
7
|
+
class ProspectiveEngine
|
|
8
|
+
attr_reader :intentions
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@intentions = {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create_intention(description:, trigger_type:, trigger_condition:,
|
|
15
|
+
urgency: Constants::DEFAULT_URGENCY, domain: nil, expires_at: nil)
|
|
16
|
+
evict_oldest! if @intentions.size >= Constants::MAX_INTENTIONS
|
|
17
|
+
|
|
18
|
+
intention = Intention.new(
|
|
19
|
+
description: description,
|
|
20
|
+
trigger_type: trigger_type,
|
|
21
|
+
trigger_condition: trigger_condition,
|
|
22
|
+
urgency: urgency,
|
|
23
|
+
domain: domain,
|
|
24
|
+
expires_at: expires_at
|
|
25
|
+
)
|
|
26
|
+
@intentions[intention.id] = intention
|
|
27
|
+
intention
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def monitor_intention(intention_id:)
|
|
31
|
+
intention = fetch(intention_id)
|
|
32
|
+
return nil unless intention
|
|
33
|
+
|
|
34
|
+
intention.monitor!
|
|
35
|
+
intention
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def trigger_intention(intention_id:)
|
|
39
|
+
intention = fetch(intention_id)
|
|
40
|
+
return nil unless intention
|
|
41
|
+
|
|
42
|
+
intention.trigger!
|
|
43
|
+
intention
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def execute_intention(intention_id:)
|
|
47
|
+
intention = fetch(intention_id)
|
|
48
|
+
return nil unless intention
|
|
49
|
+
|
|
50
|
+
intention.execute!
|
|
51
|
+
intention
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def cancel_intention(intention_id:)
|
|
55
|
+
intention = fetch(intention_id)
|
|
56
|
+
return nil unless intention
|
|
57
|
+
|
|
58
|
+
intention.cancel!
|
|
59
|
+
intention
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def check_expirations
|
|
63
|
+
expired_count = 0
|
|
64
|
+
@intentions.each_value do |intention|
|
|
65
|
+
next unless %i[pending monitoring].include?(intention.status) && intention.expired?
|
|
66
|
+
|
|
67
|
+
intention.expire!
|
|
68
|
+
expired_count += 1
|
|
69
|
+
end
|
|
70
|
+
expired_count
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def pending_intentions
|
|
74
|
+
by_status(:pending)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def monitoring_intentions
|
|
78
|
+
by_status(:monitoring)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def triggered_intentions
|
|
82
|
+
by_status(:triggered)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def by_domain(domain:)
|
|
86
|
+
@intentions.values.select { |i| i.domain == domain }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def by_urgency(min_urgency: 0.5)
|
|
90
|
+
@intentions.values.select { |i| i.urgency >= min_urgency }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def most_urgent(limit: 5)
|
|
94
|
+
active = @intentions.values.reject { |i| %i[executed expired cancelled].include?(i.status) }
|
|
95
|
+
active.sort_by { |i| -i.urgency }.first(limit)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def decay_all_urgency
|
|
99
|
+
@intentions.each_value do |intention|
|
|
100
|
+
next unless %i[pending monitoring].include?(intention.status)
|
|
101
|
+
|
|
102
|
+
intention.decay_urgency!
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def execution_rate
|
|
107
|
+
terminal = @intentions.values.select { |i| %i[executed expired cancelled].include?(i.status) }
|
|
108
|
+
return 0.0 if terminal.empty?
|
|
109
|
+
|
|
110
|
+
executed = terminal.count { |i| i.status == :executed }
|
|
111
|
+
(executed.to_f / terminal.size).round(10)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def intention_report
|
|
115
|
+
all = @intentions.values
|
|
116
|
+
counts = Constants::STATUS_TYPES.to_h do |s|
|
|
117
|
+
[s, all.count { |i| i.status == s }]
|
|
118
|
+
end
|
|
119
|
+
{
|
|
120
|
+
total: all.size,
|
|
121
|
+
by_status: counts,
|
|
122
|
+
execution_rate: execution_rate,
|
|
123
|
+
most_urgent: most_urgent(limit: 5).map(&:to_h)
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def to_h
|
|
128
|
+
{
|
|
129
|
+
intentions: @intentions.transform_values(&:to_h),
|
|
130
|
+
intention_count: @intentions.size,
|
|
131
|
+
execution_rate: execution_rate
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def fetch(intention_id)
|
|
138
|
+
@intentions[intention_id]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def by_status(status)
|
|
142
|
+
@intentions.values.select { |i| i.status == status }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def evict_oldest!
|
|
146
|
+
oldest_id = @intentions.min_by { |_id, i| i.created_at }&.first
|
|
147
|
+
@intentions.delete(oldest_id) if oldest_id
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module ProspectiveMemory
|
|
6
|
+
module Runners
|
|
7
|
+
module ProspectiveMemory
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def create_intention(description:, trigger_type:, trigger_condition:,
|
|
12
|
+
urgency: Helpers::Constants::DEFAULT_URGENCY,
|
|
13
|
+
domain: nil, expires_at: nil, **)
|
|
14
|
+
unless Helpers::Constants::TRIGGER_TYPES.include?(trigger_type)
|
|
15
|
+
return { error: :invalid_trigger_type, valid_types: Helpers::Constants::TRIGGER_TYPES }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
intention = prospective_engine.create_intention(
|
|
19
|
+
description: description,
|
|
20
|
+
trigger_type: trigger_type,
|
|
21
|
+
trigger_condition: trigger_condition,
|
|
22
|
+
urgency: urgency,
|
|
23
|
+
domain: domain,
|
|
24
|
+
expires_at: expires_at
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
Legion::Logging.debug "[prospective_memory] created intention id=#{intention.id[0..7]} " \
|
|
28
|
+
"type=#{trigger_type} urgency=#{intention.urgency.round(2)}"
|
|
29
|
+
|
|
30
|
+
{ created: true, intention: intention.to_h }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def monitor_intention(intention_id:, **)
|
|
34
|
+
intention = prospective_engine.monitor_intention(intention_id: intention_id)
|
|
35
|
+
if intention
|
|
36
|
+
Legion::Logging.debug "[prospective_memory] monitoring intention id=#{intention_id[0..7]}"
|
|
37
|
+
{ updated: true, intention: intention.to_h }
|
|
38
|
+
else
|
|
39
|
+
{ updated: false, reason: :not_found }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def trigger_intention(intention_id:, **)
|
|
44
|
+
intention = prospective_engine.trigger_intention(intention_id: intention_id)
|
|
45
|
+
if intention
|
|
46
|
+
Legion::Logging.info "[prospective_memory] triggered intention id=#{intention_id[0..7]}"
|
|
47
|
+
{ updated: true, intention: intention.to_h }
|
|
48
|
+
else
|
|
49
|
+
{ updated: false, reason: :not_found }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def execute_intention(intention_id:, **)
|
|
54
|
+
intention = prospective_engine.execute_intention(intention_id: intention_id)
|
|
55
|
+
if intention
|
|
56
|
+
Legion::Logging.info "[prospective_memory] executed intention id=#{intention_id[0..7]}"
|
|
57
|
+
{ updated: true, intention: intention.to_h }
|
|
58
|
+
else
|
|
59
|
+
{ updated: false, reason: :not_found }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def cancel_intention(intention_id:, **)
|
|
64
|
+
intention = prospective_engine.cancel_intention(intention_id: intention_id)
|
|
65
|
+
if intention
|
|
66
|
+
Legion::Logging.debug "[prospective_memory] cancelled intention id=#{intention_id[0..7]}"
|
|
67
|
+
{ updated: true, intention: intention.to_h }
|
|
68
|
+
else
|
|
69
|
+
{ updated: false, reason: :not_found }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def check_expirations(**)
|
|
74
|
+
expired_count = prospective_engine.check_expirations
|
|
75
|
+
Legion::Logging.debug "[prospective_memory] expiration check expired=#{expired_count}"
|
|
76
|
+
{ expired_count: expired_count }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def pending_intentions(**)
|
|
80
|
+
intentions = prospective_engine.pending_intentions
|
|
81
|
+
Legion::Logging.debug "[prospective_memory] pending count=#{intentions.size}"
|
|
82
|
+
{ intentions: intentions.map(&:to_h), count: intentions.size }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def monitoring_intentions(**)
|
|
86
|
+
intentions = prospective_engine.monitoring_intentions
|
|
87
|
+
Legion::Logging.debug "[prospective_memory] monitoring count=#{intentions.size}"
|
|
88
|
+
{ intentions: intentions.map(&:to_h), count: intentions.size }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def triggered_intentions(**)
|
|
92
|
+
intentions = prospective_engine.triggered_intentions
|
|
93
|
+
Legion::Logging.debug "[prospective_memory] triggered count=#{intentions.size}"
|
|
94
|
+
{ intentions: intentions.map(&:to_h), count: intentions.size }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def intentions_by_domain(domain:, **)
|
|
98
|
+
intentions = prospective_engine.by_domain(domain: domain)
|
|
99
|
+
Legion::Logging.debug "[prospective_memory] by_domain domain=#{domain} count=#{intentions.size}"
|
|
100
|
+
{ intentions: intentions.map(&:to_h), count: intentions.size, domain: domain }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def intentions_by_urgency(min_urgency: 0.5, **)
|
|
104
|
+
intentions = prospective_engine.by_urgency(min_urgency: min_urgency)
|
|
105
|
+
Legion::Logging.debug "[prospective_memory] by_urgency min=#{min_urgency} count=#{intentions.size}"
|
|
106
|
+
{ intentions: intentions.map(&:to_h), count: intentions.size }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def most_urgent_intentions(limit: 5, **)
|
|
110
|
+
intentions = prospective_engine.most_urgent(limit: limit)
|
|
111
|
+
Legion::Logging.debug "[prospective_memory] most_urgent limit=#{limit} found=#{intentions.size}"
|
|
112
|
+
{ intentions: intentions.map(&:to_h), count: intentions.size }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def decay_urgency(**)
|
|
116
|
+
prospective_engine.decay_all_urgency
|
|
117
|
+
Legion::Logging.debug '[prospective_memory] urgency decay cycle complete'
|
|
118
|
+
{ decayed: true }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def execution_rate(**)
|
|
122
|
+
rate = prospective_engine.execution_rate
|
|
123
|
+
Legion::Logging.debug "[prospective_memory] execution_rate=#{rate.round(4)}"
|
|
124
|
+
{ execution_rate: rate }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def intention_report(**)
|
|
128
|
+
report = prospective_engine.intention_report
|
|
129
|
+
Legion::Logging.debug "[prospective_memory] report total=#{report[:total]}"
|
|
130
|
+
report
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def prospective_engine
|
|
136
|
+
@prospective_engine ||= Helpers::ProspectiveEngine.new
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/prospective_memory/version'
|
|
4
|
+
require 'legion/extensions/prospective_memory/helpers/constants'
|
|
5
|
+
require 'legion/extensions/prospective_memory/helpers/intention'
|
|
6
|
+
require 'legion/extensions/prospective_memory/helpers/prospective_engine'
|
|
7
|
+
require 'legion/extensions/prospective_memory/runners/prospective_memory'
|
|
8
|
+
require 'legion/extensions/prospective_memory/client'
|
|
9
|
+
|
|
10
|
+
module Legion
|
|
11
|
+
module Extensions
|
|
12
|
+
module ProspectiveMemory
|
|
13
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/prospective_memory/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::ProspectiveMemory::Client do
|
|
6
|
+
it 'responds to all runner methods' do
|
|
7
|
+
client = described_class.new
|
|
8
|
+
expect(client).to respond_to(:create_intention)
|
|
9
|
+
expect(client).to respond_to(:monitor_intention)
|
|
10
|
+
expect(client).to respond_to(:trigger_intention)
|
|
11
|
+
expect(client).to respond_to(:execute_intention)
|
|
12
|
+
expect(client).to respond_to(:cancel_intention)
|
|
13
|
+
expect(client).to respond_to(:check_expirations)
|
|
14
|
+
expect(client).to respond_to(:pending_intentions)
|
|
15
|
+
expect(client).to respond_to(:monitoring_intentions)
|
|
16
|
+
expect(client).to respond_to(:triggered_intentions)
|
|
17
|
+
expect(client).to respond_to(:intentions_by_domain)
|
|
18
|
+
expect(client).to respond_to(:intentions_by_urgency)
|
|
19
|
+
expect(client).to respond_to(:most_urgent_intentions)
|
|
20
|
+
expect(client).to respond_to(:decay_urgency)
|
|
21
|
+
expect(client).to respond_to(:execution_rate)
|
|
22
|
+
expect(client).to respond_to(:intention_report)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::ProspectiveMemory::Helpers::Constants do
|
|
4
|
+
describe 'MAX_INTENTIONS' do
|
|
5
|
+
it 'is 300' do
|
|
6
|
+
expect(described_class::MAX_INTENTIONS).to eq(300)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
describe 'DEFAULT_URGENCY' do
|
|
11
|
+
it 'is 0.5' do
|
|
12
|
+
expect(described_class::DEFAULT_URGENCY).to eq(0.5)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe 'URGENCY_DECAY' do
|
|
17
|
+
it 'is 0.01' do
|
|
18
|
+
expect(described_class::URGENCY_DECAY).to eq(0.01)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe 'URGENCY_BOOST' do
|
|
23
|
+
it 'is 0.1' do
|
|
24
|
+
expect(described_class::URGENCY_BOOST).to eq(0.1)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe 'CHECK_INTERVAL' do
|
|
29
|
+
it 'is 60' do
|
|
30
|
+
expect(described_class::CHECK_INTERVAL).to eq(60)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe 'URGENCY_LABELS' do
|
|
35
|
+
it 'maps 0.9 to :critical' do
|
|
36
|
+
label = described_class::URGENCY_LABELS.find { |range, _| range.cover?(0.9) }&.last
|
|
37
|
+
expect(label).to eq(:critical)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'maps 0.7 to :high' do
|
|
41
|
+
label = described_class::URGENCY_LABELS.find { |range, _| range.cover?(0.7) }&.last
|
|
42
|
+
expect(label).to eq(:high)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'maps 0.5 to :moderate' do
|
|
46
|
+
label = described_class::URGENCY_LABELS.find { |range, _| range.cover?(0.5) }&.last
|
|
47
|
+
expect(label).to eq(:moderate)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'maps 0.3 to :low' do
|
|
51
|
+
label = described_class::URGENCY_LABELS.find { |range, _| range.cover?(0.3) }&.last
|
|
52
|
+
expect(label).to eq(:low)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'maps 0.1 to :deferred' do
|
|
56
|
+
label = described_class::URGENCY_LABELS.find { |range, _| range.cover?(0.1) }&.last
|
|
57
|
+
expect(label).to eq(:deferred)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'is frozen' do
|
|
61
|
+
expect(described_class::URGENCY_LABELS).to be_frozen
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe 'STATUS_TYPES' do
|
|
66
|
+
it 'includes all expected statuses' do
|
|
67
|
+
expect(described_class::STATUS_TYPES).to include(:pending, :monitoring, :triggered, :executed, :expired, :cancelled)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'is frozen' do
|
|
71
|
+
expect(described_class::STATUS_TYPES).to be_frozen
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe 'TRIGGER_TYPES' do
|
|
76
|
+
it 'includes all expected trigger types' do
|
|
77
|
+
expect(described_class::TRIGGER_TYPES).to include(:time_based, :event_based, :context_based, :activity_based)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'is frozen' do
|
|
81
|
+
expect(described_class::TRIGGER_TYPES).to be_frozen
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|