lex-episodic-buffer 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: 1cd282b5729f073c270e41c5c9e96c351297f6fec517ac799d10a8357b908e45
4
+ data.tar.gz: bd8a210f73d8577c0e98eb5c040797f1e1dcc06592cace5a9c5af95f70488c41
5
+ SHA512:
6
+ metadata.gz: 5c1841812d99d688556a87112f239909c8d8fb98ff6633c6269d29be81c26db79633597e4c4c9e9a56c42f89446a72abae1246ffb28f8603484865687b63744d
7
+ data.tar.gz: a2ffe44c971e1ee908855949938f461cfab27a9d4ff9244e9ed3bef00f5e64586f5bad10f6fd956c478d1053c64da2087e49297469f6b3f603bac68ef12fea85
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/actors/every'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module EpisodicBuffer
8
+ module Actor
9
+ class Decay < Legion::Extensions::Actors::Every
10
+ def runner_class
11
+ Legion::Extensions::EpisodicBuffer::Runners::EpisodicBuffer
12
+ end
13
+
14
+ def runner_function
15
+ 'update_episodic_buffer'
16
+ end
17
+
18
+ def time
19
+ 15
20
+ end
21
+
22
+ def run_now?
23
+ false
24
+ end
25
+
26
+ def use_runner?
27
+ false
28
+ end
29
+
30
+ def check_subtask?
31
+ false
32
+ end
33
+
34
+ def generate_task?
35
+ false
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/episodic_buffer/runners/episodic_buffer'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module EpisodicBuffer
8
+ class Client
9
+ include Legion::Extensions::EpisodicBuffer::Runners::EpisodicBuffer
10
+
11
+ def initialize(store: nil, **)
12
+ @default_store = store || Helpers::EpisodicStore.new
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :default_store
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module EpisodicBuffer
6
+ module Helpers
7
+ module Constants
8
+ MAX_EPISODES = 30
9
+ MAX_BINDINGS_PER_EPISODE = 10
10
+ MAX_HISTORY = 200
11
+ EPISODE_TTL = 120
12
+ BINDING_STRENGTH_FLOOR = 0.05
13
+ BINDING_DECAY = 0.015
14
+ DEFAULT_BINDING_STRENGTH = 0.5
15
+ ATTENTION_BOOST = 0.2
16
+ REHEARSAL_BOOST = 0.15
17
+ INTEGRATION_THRESHOLD = 0.4
18
+ RECENTLY_ACCESSED_WINDOW = 30
19
+
20
+ MODALITIES = %i[verbal visual spatial semantic emotional procedural temporal].freeze
21
+
22
+ COHERENCE_LABELS = {
23
+ (0.0...0.3) => :fragmented,
24
+ (0.3...0.6) => :partial,
25
+ (0.6...0.85) => :coherent,
26
+ (0.85..1.0) => :vivid
27
+ }.freeze
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module EpisodicBuffer
8
+ module Helpers
9
+ class Episode
10
+ include Constants
11
+
12
+ attr_reader :id, :bindings, :created_at, :last_accessed
13
+
14
+ def initialize
15
+ @id = SecureRandom.uuid
16
+ @bindings = {}
17
+ @created_at = Time.now
18
+ @last_accessed = Time.now
19
+ end
20
+
21
+ def add_binding(modality:, content:, source:, strength: DEFAULT_BINDING_STRENGTH)
22
+ return { added: false, reason: :capacity_full } if @bindings.size >= MAX_BINDINGS_PER_EPISODE
23
+
24
+ binding = EpisodicBinding.new(modality: modality, content: content, source: source, strength: strength)
25
+ @bindings[binding.id] = binding
26
+ { added: true, binding_id: binding.id }
27
+ end
28
+
29
+ def remove_binding(binding_id:)
30
+ removed = !@bindings.delete(binding_id).nil?
31
+ { removed: removed, binding_id: binding_id }
32
+ end
33
+
34
+ def attend
35
+ @last_accessed = Time.now
36
+ @bindings.each_value { |b| b.strengthen(ATTENTION_BOOST) }
37
+ end
38
+
39
+ def rehearse
40
+ @last_accessed = Time.now
41
+ @bindings.each_value { |b| b.strengthen(REHEARSAL_BOOST) }
42
+ end
43
+
44
+ def modalities_present
45
+ @bindings.values.map(&:modality).uniq
46
+ end
47
+
48
+ def coherence
49
+ integrated = @bindings.values.select(&:integrated?)
50
+ return 0.0 if integrated.empty?
51
+
52
+ integrated.sum(&:strength) / integrated.size
53
+ end
54
+
55
+ def coherence_label
56
+ score = coherence
57
+ COHERENCE_LABELS.each do |range, label|
58
+ return label if range.cover?(score)
59
+ end
60
+ :fragmented
61
+ end
62
+
63
+ def multimodal?
64
+ modalities_present.size >= 2
65
+ end
66
+
67
+ def expired?
68
+ age = Time.now - @created_at
69
+ age > EPISODE_TTL && !recently_accessed?
70
+ end
71
+
72
+ def decay_bindings
73
+ @bindings.each_value(&:decay)
74
+ @bindings.delete_if { |_, b| b.faded? }
75
+ end
76
+
77
+ def to_h
78
+ {
79
+ id: @id,
80
+ bindings: @bindings.transform_values(&:to_h),
81
+ created_at: @created_at,
82
+ last_accessed: @last_accessed,
83
+ modalities: modalities_present,
84
+ coherence: coherence,
85
+ coherence_label: coherence_label,
86
+ multimodal: multimodal?,
87
+ binding_count: @bindings.size
88
+ }
89
+ end
90
+
91
+ private
92
+
93
+ def recently_accessed?
94
+ (Time.now - @last_accessed) <= RECENTLY_ACCESSED_WINDOW
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module EpisodicBuffer
8
+ module Helpers
9
+ class EpisodicBinding
10
+ include Constants
11
+
12
+ attr_reader :id, :modality, :content, :source, :strength
13
+
14
+ def initialize(modality:, content:, source:, strength: DEFAULT_BINDING_STRENGTH)
15
+ raise ArgumentError, "invalid modality: #{modality}" unless MODALITIES.include?(modality.to_sym)
16
+
17
+ @id = SecureRandom.uuid
18
+ @modality = modality.to_sym
19
+ @content = content
20
+ @source = source.to_sym
21
+ @strength = strength.to_f.clamp(0.0, 1.0)
22
+ end
23
+
24
+ def decay
25
+ @strength = [@strength - BINDING_DECAY, 0.0].max
26
+ end
27
+
28
+ def strengthen(amount)
29
+ @strength = [@strength + amount.to_f, 1.0].min
30
+ end
31
+
32
+ def integrated?
33
+ @strength >= INTEGRATION_THRESHOLD
34
+ end
35
+
36
+ def faded?
37
+ @strength <= BINDING_STRENGTH_FLOOR
38
+ end
39
+
40
+ def to_h
41
+ {
42
+ id: @id,
43
+ modality: @modality,
44
+ content: @content,
45
+ source: @source,
46
+ strength: @strength
47
+ }
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module EpisodicBuffer
6
+ module Helpers
7
+ class EpisodicStore
8
+ include Constants
9
+
10
+ attr_reader :episodes, :history
11
+
12
+ def initialize
13
+ @episodes = {}
14
+ @history = []
15
+ end
16
+
17
+ def create_episode
18
+ evict_if_full
19
+ episode = Episode.new
20
+ @episodes[episode.id] = episode
21
+ record_history(:create, episode.id)
22
+ episode
23
+ end
24
+
25
+ def add_to_episode(episode_id:, modality:, content:, source:, strength: DEFAULT_BINDING_STRENGTH)
26
+ episode = @episodes[episode_id]
27
+ return { success: false, reason: :episode_not_found } unless episode
28
+
29
+ result = episode.add_binding(modality: modality, content: content, source: source, strength: strength)
30
+ record_history(:add_binding, episode_id) if result[:added]
31
+ result
32
+ end
33
+
34
+ def attend_episode(episode_id:)
35
+ episode = @episodes[episode_id]
36
+ return { success: false, reason: :episode_not_found } unless episode
37
+
38
+ episode.attend
39
+ record_history(:attend, episode_id)
40
+ { success: true, episode_id: episode_id }
41
+ end
42
+
43
+ def rehearse_episode(episode_id:)
44
+ episode = @episodes[episode_id]
45
+ return { success: false, reason: :episode_not_found } unless episode
46
+
47
+ episode.rehearse
48
+ record_history(:rehearse, episode_id)
49
+ { success: true, episode_id: episode_id }
50
+ end
51
+
52
+ def integrate(episode_id:)
53
+ episode = @episodes[episode_id]
54
+ return { integrated: false, reason: :episode_not_found } unless episode
55
+
56
+ coherence = episode.coherence
57
+ integrated = coherence >= INTEGRATION_THRESHOLD
58
+ {
59
+ integrated: integrated,
60
+ episode_id: episode_id,
61
+ coherence: coherence,
62
+ coherence_label: episode.coherence_label
63
+ }
64
+ end
65
+
66
+ def retrieve_by_modality(modality:)
67
+ mod = modality.to_sym
68
+ @episodes.values.select { |ep| ep.modalities_present.include?(mod) }
69
+ end
70
+
71
+ def retrieve_multimodal
72
+ @episodes.values.select(&:multimodal?)
73
+ end
74
+
75
+ def most_coherent(limit: 5)
76
+ @episodes.values
77
+ .sort_by { |ep| -ep.coherence }
78
+ .first(limit)
79
+ end
80
+
81
+ def tick
82
+ expired_count = 0
83
+ @episodes.delete_if do |_, ep|
84
+ ep.decay_bindings
85
+ if ep.expired?
86
+ expired_count += 1
87
+ true
88
+ else
89
+ false
90
+ end
91
+ end
92
+ { decayed: @episodes.size, expired: expired_count }
93
+ end
94
+
95
+ def count
96
+ @episodes.size
97
+ end
98
+
99
+ def to_h
100
+ {
101
+ episode_count: @episodes.size,
102
+ history_size: @history.size,
103
+ multimodal_count: retrieve_multimodal.size,
104
+ avg_coherence: average_coherence
105
+ }
106
+ end
107
+
108
+ private
109
+
110
+ def evict_if_full
111
+ return unless @episodes.size >= MAX_EPISODES
112
+
113
+ expired = @episodes.values.select(&:expired?)
114
+ if expired.any?
115
+ oldest = expired.min_by(&:created_at)
116
+ @episodes.delete(oldest.id)
117
+ else
118
+ lowest = @episodes.values.min_by(&:coherence)
119
+ @episodes.delete(lowest.id) if lowest
120
+ end
121
+ end
122
+
123
+ def record_history(event, episode_id)
124
+ @history << { event: event, episode_id: episode_id, at: Time.now }
125
+ @history.shift if @history.size > MAX_HISTORY
126
+ end
127
+
128
+ def average_coherence
129
+ return 0.0 if @episodes.empty?
130
+
131
+ @episodes.values.sum(&:coherence) / @episodes.size
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module EpisodicBuffer
6
+ module Runners
7
+ module EpisodicBuffer
8
+ def create_episode(**)
9
+ store = default_store
10
+ episode = store.create_episode
11
+ Legion::Logging.debug "[episodic_buffer] created episode id=#{episode.id[0..7]}"
12
+ { success: true, episode_id: episode.id, created_at: episode.created_at }
13
+ end
14
+
15
+ def add_binding(episode_id:, modality:, content:, source:,
16
+ strength: Helpers::Constants::DEFAULT_BINDING_STRENGTH, **)
17
+ unless Helpers::Constants::MODALITIES.include?(modality.to_sym)
18
+ raise ArgumentError, "invalid modality: #{modality}"
19
+ end
20
+
21
+ store = default_store
22
+ result = store.add_to_episode(
23
+ episode_id: episode_id,
24
+ modality: modality.to_sym,
25
+ content: content,
26
+ source: source,
27
+ strength: strength
28
+ )
29
+
30
+ if result[:added]
31
+ Legion::Logging.debug "[episodic_buffer] add_binding ep=#{episode_id[0..7]} mod=#{modality}"
32
+ { success: true, episode_id: episode_id, binding_id: result[:binding_id] }
33
+ else
34
+ Legion::Logging.debug "[episodic_buffer] add_binding failed ep=#{episode_id[0..7]} r=#{result[:reason]}"
35
+ { success: false, episode_id: episode_id, reason: result[:reason] }
36
+ end
37
+ end
38
+
39
+ def attend_episode(episode_id:, **)
40
+ store = default_store
41
+ result = store.attend_episode(episode_id: episode_id)
42
+ Legion::Logging.debug "[episodic_buffer] attend ep=#{episode_id[0..7]} success=#{result[:success]}"
43
+ result.merge(success: result[:success])
44
+ end
45
+
46
+ def rehearse_episode(episode_id:, **)
47
+ store = default_store
48
+ result = store.rehearse_episode(episode_id: episode_id)
49
+ Legion::Logging.debug "[episodic_buffer] rehearse ep=#{episode_id[0..7]} success=#{result[:success]}"
50
+ result.merge(success: result[:success])
51
+ end
52
+
53
+ def check_integration(episode_id:, **)
54
+ store = default_store
55
+ result = store.integrate(episode_id: episode_id)
56
+ Legion::Logging.debug "[episodic_buffer] check_integration ep=#{episode_id[0..7]} ok=#{result[:integrated]}"
57
+ result.merge(success: true)
58
+ end
59
+
60
+ def retrieve_by_modality(modality:, **)
61
+ store = default_store
62
+ episodes = store.retrieve_by_modality(modality: modality.to_sym)
63
+ Legion::Logging.debug "[episodic_buffer] retrieve_by_modality mod=#{modality} count=#{episodes.size}"
64
+ { success: true, modality: modality, count: episodes.size, episodes: episodes.map(&:to_h) }
65
+ end
66
+
67
+ def retrieve_multimodal(**)
68
+ store = default_store
69
+ episodes = store.retrieve_multimodal
70
+ Legion::Logging.debug "[episodic_buffer] retrieve_multimodal count=#{episodes.size}"
71
+ { success: true, count: episodes.size, episodes: episodes.map(&:to_h) }
72
+ end
73
+
74
+ def most_coherent(limit: 5, **)
75
+ store = default_store
76
+ episodes = store.most_coherent(limit: limit)
77
+ Legion::Logging.debug "[episodic_buffer] most_coherent limit=#{limit} returned=#{episodes.size}"
78
+ { success: true, count: episodes.size, episodes: episodes.map(&:to_h) }
79
+ end
80
+
81
+ def update_episodic_buffer(**)
82
+ store = default_store
83
+ result = store.tick
84
+ Legion::Logging.debug "[episodic_buffer] tick decayed=#{result[:decayed]} expired=#{result[:expired]}"
85
+ { success: true }.merge(result)
86
+ end
87
+
88
+ def episodic_buffer_stats(**)
89
+ store = default_store
90
+ stats = store.to_h
91
+ Legion::Logging.debug "[episodic_buffer] stats episodes=#{stats[:episode_count]}"
92
+ { success: true }.merge(stats)
93
+ end
94
+
95
+ private
96
+
97
+ def default_store
98
+ @default_store ||= Helpers::EpisodicStore.new
99
+ end
100
+
101
+ include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module EpisodicBuffer
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/episodic_buffer/version'
4
+ require 'legion/extensions/episodic_buffer/helpers/constants'
5
+ require 'legion/extensions/episodic_buffer/helpers/episodic_binding'
6
+ require 'legion/extensions/episodic_buffer/helpers/episode'
7
+ require 'legion/extensions/episodic_buffer/helpers/episodic_store'
8
+ require 'legion/extensions/episodic_buffer/runners/episodic_buffer'
9
+
10
+ module Legion
11
+ module Extensions
12
+ module EpisodicBuffer
13
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
14
+ end
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-episodic-buffer
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: Baddeley's episodic buffer — binds multimodal information into coherent
27
+ episodic representations for the LegionIO cognitive architecture
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/legion/extensions/episodic_buffer.rb
35
+ - lib/legion/extensions/episodic_buffer/actors/decay.rb
36
+ - lib/legion/extensions/episodic_buffer/client.rb
37
+ - lib/legion/extensions/episodic_buffer/helpers/constants.rb
38
+ - lib/legion/extensions/episodic_buffer/helpers/episode.rb
39
+ - lib/legion/extensions/episodic_buffer/helpers/episodic_binding.rb
40
+ - lib/legion/extensions/episodic_buffer/helpers/episodic_store.rb
41
+ - lib/legion/extensions/episodic_buffer/runners/episodic_buffer.rb
42
+ - lib/legion/extensions/episodic_buffer/version.rb
43
+ homepage: https://github.com/LegionIO/lex-episodic-buffer
44
+ licenses:
45
+ - MIT
46
+ metadata:
47
+ homepage_uri: https://github.com/LegionIO/lex-episodic-buffer
48
+ source_code_uri: https://github.com/LegionIO/lex-episodic-buffer
49
+ documentation_uri: https://github.com/LegionIO/lex-episodic-buffer
50
+ changelog_uri: https://github.com/LegionIO/lex-episodic-buffer
51
+ bug_tracker_uri: https://github.com/LegionIO/lex-episodic-buffer/issues
52
+ rubygems_mfa_required: 'true'
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '3.4'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 3.6.9
68
+ specification_version: 4
69
+ summary: LEX Episodic Buffer
70
+ test_files: []