lex-semantic-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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '078f5ae8f0283c2644b9713a80bb865bdd750ec02a60a6e3aa2f980a81541bc9'
4
+ data.tar.gz: af9949496a45f4c85e10a47754a939b5b746be2892dd206394fc72fd521e3c58
5
+ SHA512:
6
+ metadata.gz: 62b9c20fc08fdae67e52da0283126eb26c940307973e4bb1a91f14cda435f940b58b3eeb372d6137220bedff52ec6388044140c5d786cf86d1cf7de713fc1b2c
7
+ data.tar.gz: f677725f1951be5496ca865485835f1e94ade8d30e1eba60dc064ea619242a5238e2ae68651e1abfb82cf61ed881e2f63f2323763152db85b75be9d810d5fe8f
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,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/semantic_memory/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-semantic-memory'
7
+ spec.version = Legion::Extensions::SemanticMemory::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Semantic Memory'
12
+ spec.description = 'Tulving semantic memory store for brain-modeled agentic AI — concept storage, ' \
13
+ 'taxonomic relations (is_a, has_a, part_of), spreading activation retrieval, ' \
14
+ 'and knowledge consolidation with confidence-based decay.'
15
+ spec.homepage = 'https://github.com/LegionIO/lex-semantic-memory'
16
+ spec.license = 'MIT'
17
+ spec.required_ruby_version = '>= 3.4'
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-semantic-memory'
21
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-semantic-memory'
22
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-semantic-memory'
23
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-semantic-memory/issues'
24
+ spec.metadata['rubygems_mfa_required'] = 'true'
25
+
26
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
27
+ Dir.glob('{lib,spec}/**/*') + %w[lex-semantic-memory.gemspec Gemfile]
28
+ end
29
+ spec.require_paths = ['lib']
30
+ spec.add_development_dependency 'legion-gaia'
31
+ end
@@ -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 SemanticMemory
8
+ module Actor
9
+ class Decay < Legion::Extensions::Actors::Every
10
+ def runner_class
11
+ Legion::Extensions::SemanticMemory::Runners::SemanticMemory
12
+ end
13
+
14
+ def runner_function
15
+ 'update_semantic_memory'
16
+ end
17
+
18
+ def time
19
+ 300
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,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/semantic_memory/helpers/constants'
4
+ require 'legion/extensions/semantic_memory/helpers/concept'
5
+ require 'legion/extensions/semantic_memory/helpers/knowledge_store'
6
+ require 'legion/extensions/semantic_memory/runners/semantic_memory'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module SemanticMemory
11
+ class Client
12
+ include Runners::SemanticMemory
13
+
14
+ def initialize(knowledge_store: nil, **)
15
+ @knowledge_store = knowledge_store || Helpers::KnowledgeStore.new
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :knowledge_store
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module SemanticMemory
6
+ module Helpers
7
+ class Concept
8
+ attr_reader :id, :name, :domain, :properties, :relations, :created_at
9
+ attr_accessor :confidence, :access_count
10
+
11
+ def initialize(name:, domain: :general, confidence: nil, properties: {})
12
+ @id = SecureRandom.uuid
13
+ @name = name
14
+ @domain = domain
15
+ @confidence = (confidence || Constants::DEFAULT_CONFIDENCE).clamp(0.0, 1.0)
16
+ @properties = properties.dup
17
+ @relations = []
18
+ @access_count = 0
19
+ @created_at = Time.now.utc
20
+ end
21
+
22
+ def add_relation(type:, target_name:, confidence: nil)
23
+ return false unless Constants::RELATION_TYPES.include?(type)
24
+
25
+ existing = @relations.find { |r| r[:type] == type && r[:target] == target_name }
26
+ if existing
27
+ existing[:confidence] = [existing[:confidence] + Constants::ACCESS_BOOST, 1.0].min
28
+ return existing
29
+ end
30
+
31
+ trim_relations if @relations.size >= Constants::MAX_RELATIONS_PER_CONCEPT
32
+ rel = { type: type, target: target_name, confidence: (confidence || Constants::DEFAULT_CONFIDENCE).clamp(0.0, 1.0) }
33
+ @relations << rel
34
+ rel
35
+ end
36
+
37
+ def relations_of_type(type)
38
+ @relations.select { |r| r[:type] == type }
39
+ end
40
+
41
+ def related_concepts
42
+ @relations.map { |r| r[:target] }.uniq
43
+ end
44
+
45
+ def set_property(key, value)
46
+ @properties[key] = value
47
+ end
48
+
49
+ def get_property(key)
50
+ @properties[key]
51
+ end
52
+
53
+ def access
54
+ @access_count += 1
55
+ @confidence = [@confidence + Constants::ACCESS_BOOST, 1.0].min
56
+ end
57
+
58
+ def decay
59
+ @confidence = [@confidence - Constants::CONFIDENCE_DECAY, Constants::CONFIDENCE_FLOOR].max
60
+ @relations.each { |r| r[:confidence] = [r[:confidence] - Constants::CONFIDENCE_DECAY, Constants::CONFIDENCE_FLOOR].max }
61
+ @relations.reject! { |r| r[:confidence] <= Constants::CONFIDENCE_FLOOR }
62
+ end
63
+
64
+ def faded?
65
+ @confidence <= Constants::CONFIDENCE_FLOOR
66
+ end
67
+
68
+ def label
69
+ Constants::CONFIDENCE_LABELS.each { |range, lbl| return lbl if range.cover?(@confidence) }
70
+ :uncertain
71
+ end
72
+
73
+ def to_h
74
+ {
75
+ id: @id,
76
+ name: @name,
77
+ domain: @domain,
78
+ confidence: @confidence,
79
+ properties: @properties,
80
+ relations: @relations,
81
+ access_count: @access_count,
82
+ label: label,
83
+ created_at: @created_at
84
+ }
85
+ end
86
+
87
+ private
88
+
89
+ def trim_relations
90
+ @relations.sort_by! { |r| r[:confidence] }
91
+ @relations.shift
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module SemanticMemory
6
+ module Helpers
7
+ module Constants
8
+ MAX_CONCEPTS = 500
9
+ MAX_RELATIONS_PER_CONCEPT = 50
10
+ MAX_HISTORY = 200
11
+
12
+ # Relation types between concepts
13
+ RELATION_TYPES = %i[
14
+ is_a has_a part_of
15
+ property_of used_for
16
+ causes prevents
17
+ similar_to opposite_of
18
+ instance_of category_of
19
+ ].freeze
20
+
21
+ # Confidence thresholds
22
+ DEFAULT_CONFIDENCE = 0.5
23
+ CONFIDENCE_FLOOR = 0.05
24
+ CONFIDENCE_DECAY = 0.005
25
+ CONFIDENCE_ALPHA = 0.12
26
+
27
+ # Access frequency tracking
28
+ ACCESS_BOOST = 0.05
29
+ ACCESS_DECAY = 0.01
30
+
31
+ # Retrieval spreading activation
32
+ SPREAD_FACTOR = 0.6
33
+ MAX_SPREAD_HOPS = 3
34
+ SPREAD_THRESHOLD = 0.1
35
+
36
+ CONFIDENCE_LABELS = {
37
+ (0.8..) => :established,
38
+ (0.6...0.8) => :reliable,
39
+ (0.4...0.6) => :provisional,
40
+ (0.2...0.4) => :tentative,
41
+ (..0.2) => :uncertain
42
+ }.freeze
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module SemanticMemory
6
+ module Helpers
7
+ class KnowledgeStore
8
+ include Constants
9
+
10
+ attr_reader :concepts, :retrieval_history
11
+
12
+ def initialize
13
+ @concepts = {}
14
+ @retrieval_history = []
15
+ end
16
+
17
+ def store(name:, domain: :general, confidence: nil, properties: {})
18
+ if @concepts.key?(name)
19
+ existing = @concepts[name]
20
+ existing.access
21
+ properties.each { |k, v| existing.set_property(k, v) }
22
+ return existing
23
+ end
24
+
25
+ ensure_capacity
26
+ concept = Concept.new(name: name, domain: domain, confidence: confidence, properties: properties)
27
+ @concepts[name] = concept
28
+ concept
29
+ end
30
+
31
+ def relate(source:, target:, type:, confidence: nil)
32
+ store(name: source) unless @concepts.key?(source)
33
+ store(name: target) unless @concepts.key?(target)
34
+ @concepts[source].add_relation(type: type, target_name: target, confidence: confidence)
35
+ end
36
+
37
+ def retrieve(name:)
38
+ concept = @concepts[name]
39
+ return nil unless concept
40
+
41
+ concept.access
42
+ record_retrieval(name)
43
+ concept
44
+ end
45
+
46
+ def query_relations(name:, type: nil)
47
+ concept = @concepts[name]
48
+ return [] unless concept
49
+
50
+ concept.access
51
+ if type
52
+ concept.relations_of_type(type)
53
+ else
54
+ concept.relations
55
+ end
56
+ end
57
+
58
+ def check_is_a(concept_name, category_name)
59
+ rels = query_relations(name: concept_name, type: :is_a)
60
+ rels.any? { |r| r[:target] == category_name }
61
+ end
62
+
63
+ def instances_of(category_name)
64
+ @concepts.values.select do |c|
65
+ c.relations.any? { |r| r[:type] == :is_a && r[:target] == category_name }
66
+ end
67
+ end
68
+
69
+ def spreading_activation(seed:, hops: MAX_SPREAD_HOPS)
70
+ activated = {}
71
+ queue = [[seed, 1.0]]
72
+
73
+ hops.times do
74
+ next_queue = []
75
+ queue.each do |name, strength|
76
+ next if strength < SPREAD_THRESHOLD
77
+ next if activated.key?(name) && activated[name] >= strength
78
+
79
+ activated[name] = strength
80
+ concept = @concepts[name]
81
+ next unless concept
82
+
83
+ concept.related_concepts.each do |related|
84
+ next_strength = strength * SPREAD_FACTOR
85
+ next_queue << [related, next_strength] if next_strength >= SPREAD_THRESHOLD
86
+ end
87
+ end
88
+ queue = next_queue
89
+ end
90
+
91
+ activated.sort_by { |_, s| -s }.to_h
92
+ end
93
+
94
+ def concepts_in_domain(domain)
95
+ @concepts.values.select { |c| c.domain == domain }
96
+ end
97
+
98
+ def search(query)
99
+ @concepts.values.select { |c| c.name.to_s.include?(query.to_s) }
100
+ end
101
+
102
+ def decay_all
103
+ @concepts.each_value(&:decay)
104
+ @concepts.reject! { |_, c| c.faded? }
105
+ end
106
+
107
+ def concept_count
108
+ @concepts.size
109
+ end
110
+
111
+ def relation_count
112
+ @concepts.values.sum { |c| c.relations.size }
113
+ end
114
+
115
+ def to_h
116
+ {
117
+ concepts: concept_count,
118
+ relations: relation_count,
119
+ domains: @concepts.values.map(&:domain).uniq.size,
120
+ history_size: @retrieval_history.size
121
+ }
122
+ end
123
+
124
+ private
125
+
126
+ def ensure_capacity
127
+ return if @concepts.size < MAX_CONCEPTS
128
+
129
+ weakest = @concepts.min_by { |_, c| c.confidence }
130
+ @concepts.delete(weakest.first) if weakest
131
+ end
132
+
133
+ def record_retrieval(name)
134
+ @retrieval_history << { name: name, at: Time.now.utc }
135
+ @retrieval_history.shift while @retrieval_history.size > MAX_HISTORY
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module SemanticMemory
6
+ module Runners
7
+ module SemanticMemory
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def store_concept(name:, domain: :general, confidence: nil, properties: {}, **)
12
+ concept = knowledge_store.store(name: name, domain: domain, confidence: confidence, properties: properties)
13
+ Legion::Logging.debug "[semantic_memory] store: name=#{name} domain=#{domain} conf=#{concept.confidence.round(3)}"
14
+ { success: true, concept: concept.to_h }
15
+ end
16
+
17
+ def relate_concepts(source:, target:, type:, confidence: nil, **)
18
+ type_sym = type.to_sym
19
+ result = knowledge_store.relate(source: source, target: target, type: type_sym, confidence: confidence)
20
+ Legion::Logging.debug "[semantic_memory] relate: #{source} --#{type_sym}--> #{target}"
21
+ { success: true, source: source, target: target, type: type_sym, relation: result }
22
+ end
23
+
24
+ def retrieve_concept(name:, **)
25
+ concept = knowledge_store.retrieve(name: name)
26
+ if concept
27
+ Legion::Logging.debug "[semantic_memory] retrieve: name=#{name} conf=#{concept.confidence.round(3)}"
28
+ { success: true, found: true, concept: concept.to_h }
29
+ else
30
+ Legion::Logging.debug "[semantic_memory] retrieve: name=#{name} not_found"
31
+ { success: true, found: false, name: name }
32
+ end
33
+ end
34
+
35
+ def query_concept_relations(name:, type: nil, **)
36
+ relations = knowledge_store.query_relations(name: name, type: type&.to_sym)
37
+ Legion::Logging.debug "[semantic_memory] query_relations: name=#{name} type=#{type} count=#{relations.size}"
38
+ { success: true, name: name, relations: relations, count: relations.size }
39
+ end
40
+
41
+ def check_category(concept:, category:, **)
42
+ result = knowledge_store.check_is_a(concept, category)
43
+ Legion::Logging.debug "[semantic_memory] check_category: #{concept} is_a #{category} = #{result}"
44
+ { success: true, concept: concept, category: category, is_member: result }
45
+ end
46
+
47
+ def find_instances(category:, **)
48
+ instances = knowledge_store.instances_of(category)
49
+ Legion::Logging.debug "[semantic_memory] instances_of: #{category} count=#{instances.size}"
50
+ { success: true, category: category, instances: instances.map(&:name), count: instances.size }
51
+ end
52
+
53
+ def activate_spread(seed:, hops: nil, **)
54
+ hop_count = hops || Helpers::Constants::MAX_SPREAD_HOPS
55
+ activated = knowledge_store.spreading_activation(seed: seed, hops: hop_count)
56
+ Legion::Logging.debug "[semantic_memory] spread: seed=#{seed} hops=#{hop_count} activated=#{activated.size}"
57
+ { success: true, seed: seed, activated: activated, count: activated.size }
58
+ end
59
+
60
+ def concepts_in(domain:, **)
61
+ concepts = knowledge_store.concepts_in_domain(domain)
62
+ Legion::Logging.debug "[semantic_memory] domain_query: domain=#{domain} count=#{concepts.size}"
63
+ { success: true, domain: domain, concepts: concepts.map(&:name), count: concepts.size }
64
+ end
65
+
66
+ def update_semantic_memory(**)
67
+ knowledge_store.decay_all
68
+ Legion::Logging.debug "[semantic_memory] tick: concepts=#{knowledge_store.concept_count} " \
69
+ "relations=#{knowledge_store.relation_count}"
70
+ {
71
+ success: true,
72
+ concepts: knowledge_store.concept_count,
73
+ relations: knowledge_store.relation_count
74
+ }
75
+ end
76
+
77
+ def semantic_memory_stats(**)
78
+ { success: true, stats: knowledge_store.to_h }
79
+ end
80
+
81
+ private
82
+
83
+ def knowledge_store
84
+ @knowledge_store ||= Helpers::KnowledgeStore.new
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module SemanticMemory
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'legion/extensions/semantic_memory/version'
5
+ require 'legion/extensions/semantic_memory/helpers/constants'
6
+ require 'legion/extensions/semantic_memory/helpers/concept'
7
+ require 'legion/extensions/semantic_memory/helpers/knowledge_store'
8
+ require 'legion/extensions/semantic_memory/runners/semantic_memory'
9
+ require 'legion/extensions/semantic_memory/client'
10
+
11
+ module Legion
12
+ module Extensions
13
+ module SemanticMemory
14
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::SemanticMemory::Client do
4
+ subject(:client) { described_class.new }
5
+
6
+ it 'includes Runners::SemanticMemory' do
7
+ expect(described_class.ancestors).to include(Legion::Extensions::SemanticMemory::Runners::SemanticMemory)
8
+ end
9
+
10
+ it 'supports full knowledge lifecycle' do
11
+ # Build a taxonomy
12
+ client.store_concept(name: :animal, domain: :biology, properties: { kingdom: :animalia })
13
+ client.relate_concepts(source: :mammal, target: :animal, type: :is_a)
14
+ client.relate_concepts(source: :dog, target: :mammal, type: :is_a)
15
+ client.relate_concepts(source: :cat, target: :mammal, type: :is_a)
16
+ client.relate_concepts(source: :dog, target: :tail, type: :has_a)
17
+
18
+ # Query taxonomy
19
+ expect(client.check_category(concept: :dog, category: :mammal)[:is_member]).to be true
20
+ expect(client.find_instances(category: :mammal)[:count]).to eq(2)
21
+
22
+ # Spreading activation
23
+ activated = client.activate_spread(seed: :dog)
24
+ expect(activated[:activated]).to have_key(:dog)
25
+
26
+ # Retrieve
27
+ concept = client.retrieve_concept(name: :dog)
28
+ expect(concept[:found]).to be true
29
+
30
+ # Tick
31
+ client.update_semantic_memory
32
+ stats = client.semantic_memory_stats
33
+ expect(stats[:stats][:concepts]).to be >= 3
34
+ end
35
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::SemanticMemory::Helpers::Concept do
4
+ subject(:concept) { described_class.new(name: :dog, domain: :animals) }
5
+
6
+ describe '#initialize' do
7
+ it 'assigns fields' do
8
+ expect(concept.name).to eq(:dog)
9
+ expect(concept.domain).to eq(:animals)
10
+ expect(concept.access_count).to eq(0)
11
+ end
12
+
13
+ it 'defaults confidence to DEFAULT_CONFIDENCE' do
14
+ expect(concept.confidence).to eq(Legion::Extensions::SemanticMemory::Helpers::Constants::DEFAULT_CONFIDENCE)
15
+ end
16
+
17
+ it 'assigns uuid and timestamp' do
18
+ expect(concept.id).to match(/\A[0-9a-f-]{36}\z/)
19
+ expect(concept.created_at).to be_a(Time)
20
+ end
21
+
22
+ it 'accepts custom properties' do
23
+ c = described_class.new(name: :cat, properties: { legs: 4 })
24
+ expect(c.get_property(:legs)).to eq(4)
25
+ end
26
+ end
27
+
28
+ describe '#add_relation' do
29
+ it 'adds a typed relation' do
30
+ rel = concept.add_relation(type: :is_a, target_name: :mammal)
31
+ expect(rel[:type]).to eq(:is_a)
32
+ expect(rel[:target]).to eq(:mammal)
33
+ expect(concept.relations.size).to eq(1)
34
+ end
35
+
36
+ it 'rejects invalid relation types' do
37
+ expect(concept.add_relation(type: :invalid, target_name: :thing)).to be false
38
+ end
39
+
40
+ it 'reinforces existing relation instead of duplicating' do
41
+ concept.add_relation(type: :is_a, target_name: :mammal)
42
+ concept.add_relation(type: :is_a, target_name: :mammal)
43
+ expect(concept.relations.size).to eq(1)
44
+ end
45
+ end
46
+
47
+ describe '#relations_of_type' do
48
+ it 'filters by type' do
49
+ concept.add_relation(type: :is_a, target_name: :mammal)
50
+ concept.add_relation(type: :has_a, target_name: :tail)
51
+ expect(concept.relations_of_type(:is_a).size).to eq(1)
52
+ end
53
+ end
54
+
55
+ describe '#related_concepts' do
56
+ it 'lists unique related concept names' do
57
+ concept.add_relation(type: :is_a, target_name: :mammal)
58
+ concept.add_relation(type: :has_a, target_name: :tail)
59
+ expect(concept.related_concepts).to contain_exactly(:mammal, :tail)
60
+ end
61
+ end
62
+
63
+ describe '#access' do
64
+ it 'increments count and boosts confidence' do
65
+ before = concept.confidence
66
+ concept.access
67
+ expect(concept.access_count).to eq(1)
68
+ expect(concept.confidence).to be > before
69
+ end
70
+ end
71
+
72
+ describe '#decay' do
73
+ it 'reduces confidence' do
74
+ before = concept.confidence
75
+ concept.decay
76
+ expect(concept.confidence).to be < before
77
+ end
78
+
79
+ it 'does not drop below floor' do
80
+ 100.times { concept.decay }
81
+ expect(concept.confidence).to be >= Legion::Extensions::SemanticMemory::Helpers::Constants::CONFIDENCE_FLOOR
82
+ end
83
+
84
+ it 'prunes weak relations' do
85
+ concept.add_relation(type: :is_a, target_name: :mammal, confidence: 0.06)
86
+ 100.times { concept.decay }
87
+ expect(concept.relations).to be_empty
88
+ end
89
+ end
90
+
91
+ describe '#faded?' do
92
+ it 'returns false for healthy concept' do
93
+ expect(concept.faded?).to be false
94
+ end
95
+
96
+ it 'returns true at floor' do
97
+ concept.confidence = Legion::Extensions::SemanticMemory::Helpers::Constants::CONFIDENCE_FLOOR
98
+ expect(concept.faded?).to be true
99
+ end
100
+ end
101
+
102
+ describe '#label' do
103
+ it 'returns :provisional for default confidence' do
104
+ expect(concept.label).to eq(:provisional)
105
+ end
106
+
107
+ it 'returns :established for high confidence' do
108
+ concept.confidence = 0.9
109
+ expect(concept.label).to eq(:established)
110
+ end
111
+ end
112
+
113
+ describe '#to_h' do
114
+ it 'returns hash with all fields' do
115
+ h = concept.to_h
116
+ expect(h).to include(:id, :name, :domain, :confidence, :properties, :relations, :access_count, :label)
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::SemanticMemory::Helpers::KnowledgeStore do
4
+ subject(:store) { described_class.new }
5
+
6
+ describe '#store' do
7
+ it 'creates a new concept' do
8
+ concept = store.store(name: :dog, domain: :animals)
9
+ expect(concept.name).to eq(:dog)
10
+ expect(store.concept_count).to eq(1)
11
+ end
12
+
13
+ it 'updates existing concept on re-store' do
14
+ store.store(name: :dog, domain: :animals)
15
+ store.store(name: :dog, properties: { legs: 4 })
16
+ expect(store.concept_count).to eq(1)
17
+ expect(store.retrieve(name: :dog).get_property(:legs)).to eq(4)
18
+ end
19
+ end
20
+
21
+ describe '#relate' do
22
+ it 'creates relation between concepts' do
23
+ store.relate(source: :dog, target: :mammal, type: :is_a)
24
+ rels = store.query_relations(name: :dog, type: :is_a)
25
+ expect(rels.size).to eq(1)
26
+ expect(rels.first[:target]).to eq(:mammal)
27
+ end
28
+
29
+ it 'auto-creates concepts that do not exist' do
30
+ store.relate(source: :sparrow, target: :bird, type: :is_a)
31
+ expect(store.concept_count).to eq(2)
32
+ end
33
+ end
34
+
35
+ describe '#retrieve' do
36
+ it 'returns concept and increments access' do
37
+ store.store(name: :dog, domain: :animals)
38
+ concept = store.retrieve(name: :dog)
39
+ expect(concept.access_count).to eq(1)
40
+ end
41
+
42
+ it 'returns nil for unknown concept' do
43
+ expect(store.retrieve(name: :unknown)).to be_nil
44
+ end
45
+
46
+ it 'records retrieval in history' do
47
+ store.store(name: :dog)
48
+ store.retrieve(name: :dog)
49
+ expect(store.retrieval_history.size).to eq(1)
50
+ end
51
+ end
52
+
53
+ describe '#check_is_a' do
54
+ it 'returns true for valid is_a relation' do
55
+ store.relate(source: :dog, target: :mammal, type: :is_a)
56
+ expect(store.check_is_a(:dog, :mammal)).to be true
57
+ end
58
+
59
+ it 'returns false for non-existent relation' do
60
+ store.store(name: :dog)
61
+ expect(store.check_is_a(:dog, :reptile)).to be false
62
+ end
63
+ end
64
+
65
+ describe '#instances_of' do
66
+ it 'finds all instances of a category' do
67
+ store.relate(source: :dog, target: :mammal, type: :is_a)
68
+ store.relate(source: :cat, target: :mammal, type: :is_a)
69
+ store.relate(source: :sparrow, target: :bird, type: :is_a)
70
+ instances = store.instances_of(:mammal)
71
+ expect(instances.map(&:name)).to contain_exactly(:dog, :cat)
72
+ end
73
+ end
74
+
75
+ describe '#spreading_activation' do
76
+ it 'activates seed concept' do
77
+ store.store(name: :dog)
78
+ activated = store.spreading_activation(seed: :dog)
79
+ expect(activated).to have_key(:dog)
80
+ end
81
+
82
+ it 'spreads to related concepts' do
83
+ store.relate(source: :dog, target: :mammal, type: :is_a)
84
+ store.relate(source: :mammal, target: :animal, type: :is_a)
85
+ activated = store.spreading_activation(seed: :dog, hops: 3)
86
+ expect(activated.keys).to include(:dog, :mammal)
87
+ end
88
+
89
+ it 'diminishes activation with each hop' do
90
+ store.relate(source: :dog, target: :mammal, type: :is_a)
91
+ activated = store.spreading_activation(seed: :dog)
92
+ expect(activated[:dog]).to be > activated[:mammal] if activated.key?(:mammal)
93
+ end
94
+ end
95
+
96
+ describe '#concepts_in_domain' do
97
+ it 'returns concepts in specified domain' do
98
+ store.store(name: :dog, domain: :animals)
99
+ store.store(name: :ruby, domain: :programming)
100
+ expect(store.concepts_in_domain(:animals).map(&:name)).to eq([:dog])
101
+ end
102
+ end
103
+
104
+ describe '#search' do
105
+ it 'finds concepts matching query' do
106
+ store.store(name: :golden_retriever)
107
+ store.store(name: :golden_gate)
108
+ store.store(name: :poodle)
109
+ results = store.search(:golden)
110
+ expect(results.size).to eq(2)
111
+ end
112
+ end
113
+
114
+ describe '#decay_all' do
115
+ it 'decays all concepts' do
116
+ store.store(name: :dog, confidence: 0.5)
117
+ store.decay_all
118
+ concept = store.concepts[:dog]
119
+ expect(concept.confidence).to be < 0.5 if concept
120
+ end
121
+
122
+ it 'prunes faded concepts' do
123
+ floor = Legion::Extensions::SemanticMemory::Helpers::Constants::CONFIDENCE_FLOOR
124
+ store.store(name: :weak, confidence: floor + 0.004)
125
+ store.decay_all
126
+ expect(store.concept_count).to eq(0)
127
+ end
128
+ end
129
+
130
+ describe '#to_h' do
131
+ it 'returns stats' do
132
+ store.store(name: :dog, domain: :animals)
133
+ store.relate(source: :dog, target: :mammal, type: :is_a)
134
+ h = store.to_h
135
+ expect(h).to include(:concepts, :relations, :domains, :history_size)
136
+ expect(h[:concepts]).to eq(2)
137
+ expect(h[:relations]).to eq(1)
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::SemanticMemory::Runners::SemanticMemory do
4
+ let(:client) { Legion::Extensions::SemanticMemory::Client.new }
5
+
6
+ describe '#store_concept' do
7
+ it 'stores a concept' do
8
+ result = client.store_concept(name: :dog, domain: :animals)
9
+ expect(result[:success]).to be true
10
+ expect(result[:concept][:name]).to eq(:dog)
11
+ end
12
+ end
13
+
14
+ describe '#relate_concepts' do
15
+ it 'creates a relation' do
16
+ result = client.relate_concepts(source: :dog, target: :mammal, type: :is_a)
17
+ expect(result[:success]).to be true
18
+ expect(result[:type]).to eq(:is_a)
19
+ end
20
+ end
21
+
22
+ describe '#retrieve_concept' do
23
+ it 'retrieves stored concept' do
24
+ client.store_concept(name: :dog, domain: :animals)
25
+ result = client.retrieve_concept(name: :dog)
26
+ expect(result[:found]).to be true
27
+ expect(result[:concept][:name]).to eq(:dog)
28
+ end
29
+
30
+ it 'returns found: false for unknown' do
31
+ result = client.retrieve_concept(name: :unknown)
32
+ expect(result[:found]).to be false
33
+ end
34
+ end
35
+
36
+ describe '#query_concept_relations' do
37
+ it 'returns relations' do
38
+ client.relate_concepts(source: :dog, target: :mammal, type: :is_a)
39
+ result = client.query_concept_relations(name: :dog, type: :is_a)
40
+ expect(result[:count]).to eq(1)
41
+ end
42
+ end
43
+
44
+ describe '#check_category' do
45
+ it 'checks is_a membership' do
46
+ client.relate_concepts(source: :dog, target: :mammal, type: :is_a)
47
+ result = client.check_category(concept: :dog, category: :mammal)
48
+ expect(result[:is_member]).to be true
49
+ end
50
+
51
+ it 'returns false for non-member' do
52
+ client.store_concept(name: :dog)
53
+ result = client.check_category(concept: :dog, category: :reptile)
54
+ expect(result[:is_member]).to be false
55
+ end
56
+ end
57
+
58
+ describe '#find_instances' do
59
+ it 'finds instances of category' do
60
+ client.relate_concepts(source: :dog, target: :mammal, type: :is_a)
61
+ client.relate_concepts(source: :cat, target: :mammal, type: :is_a)
62
+ result = client.find_instances(category: :mammal)
63
+ expect(result[:count]).to eq(2)
64
+ end
65
+ end
66
+
67
+ describe '#activate_spread' do
68
+ it 'spreads activation from seed' do
69
+ client.relate_concepts(source: :dog, target: :mammal, type: :is_a)
70
+ result = client.activate_spread(seed: :dog)
71
+ expect(result[:success]).to be true
72
+ expect(result[:activated]).to have_key(:dog)
73
+ end
74
+ end
75
+
76
+ describe '#concepts_in' do
77
+ it 'returns concepts in domain' do
78
+ client.store_concept(name: :dog, domain: :animals)
79
+ client.store_concept(name: :ruby, domain: :programming)
80
+ result = client.concepts_in(domain: :animals)
81
+ expect(result[:count]).to eq(1)
82
+ expect(result[:concepts]).to eq([:dog])
83
+ end
84
+ end
85
+
86
+ describe '#update_semantic_memory' do
87
+ it 'decays and returns counts' do
88
+ client.store_concept(name: :dog)
89
+ result = client.update_semantic_memory
90
+ expect(result[:success]).to be true
91
+ expect(result).to have_key(:concepts)
92
+ expect(result).to have_key(:relations)
93
+ end
94
+ end
95
+
96
+ describe '#semantic_memory_stats' do
97
+ it 'returns stats' do
98
+ result = client.semantic_memory_stats
99
+ expect(result[:success]).to be true
100
+ expect(result[:stats]).to include(:concepts, :relations, :domains)
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,20 @@
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
+ end
13
+
14
+ require 'legion/extensions/semantic_memory'
15
+
16
+ RSpec.configure do |config|
17
+ config.example_status_persistence_file_path = '.rspec_status'
18
+ config.disable_monkey_patching!
19
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
20
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-semantic-memory
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: Tulving semantic memory store for brain-modeled agentic AI — concept
27
+ storage, taxonomic relations (is_a, has_a, part_of), spreading activation retrieval,
28
+ and knowledge consolidation with confidence-based decay.
29
+ email:
30
+ - matthewdiverson@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - Gemfile
36
+ - lex-semantic-memory.gemspec
37
+ - lib/legion/extensions/semantic_memory.rb
38
+ - lib/legion/extensions/semantic_memory/actors/decay.rb
39
+ - lib/legion/extensions/semantic_memory/client.rb
40
+ - lib/legion/extensions/semantic_memory/helpers/concept.rb
41
+ - lib/legion/extensions/semantic_memory/helpers/constants.rb
42
+ - lib/legion/extensions/semantic_memory/helpers/knowledge_store.rb
43
+ - lib/legion/extensions/semantic_memory/runners/semantic_memory.rb
44
+ - lib/legion/extensions/semantic_memory/version.rb
45
+ - spec/legion/extensions/semantic_memory/client_spec.rb
46
+ - spec/legion/extensions/semantic_memory/helpers/concept_spec.rb
47
+ - spec/legion/extensions/semantic_memory/helpers/knowledge_store_spec.rb
48
+ - spec/legion/extensions/semantic_memory/runners/semantic_memory_spec.rb
49
+ - spec/spec_helper.rb
50
+ homepage: https://github.com/LegionIO/lex-semantic-memory
51
+ licenses:
52
+ - MIT
53
+ metadata:
54
+ homepage_uri: https://github.com/LegionIO/lex-semantic-memory
55
+ source_code_uri: https://github.com/LegionIO/lex-semantic-memory
56
+ documentation_uri: https://github.com/LegionIO/lex-semantic-memory
57
+ changelog_uri: https://github.com/LegionIO/lex-semantic-memory
58
+ bug_tracker_uri: https://github.com/LegionIO/lex-semantic-memory/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 Semantic Memory
77
+ test_files: []