lex-semantic-priming 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: 4e40ba76b2f97ab9c12a1b004d3e7cdb577caaf2153fb8f6e189fa873fde8651
4
+ data.tar.gz: 353c181dddb932afd5c1c48cfcbc15bd01ab22a0da68b2b9368d21ea645170cd
5
+ SHA512:
6
+ metadata.gz: c8a442c55e237f69d9d172830f9f492a6236263d96620cb499e7e2361c325718166aadf31e5ffd849ae5bb25bc467fc7a6e1989db195fb7b64143358aca71ef9
7
+ data.tar.gz: ef60c29517d3e90cae46466fa73fc98248ed4af5619d5abaf2507b664b5951b5b8bb8890462194d698568f610cbf4d31658adecf52164f60e5db7d529d6e0a73
@@ -0,0 +1,16 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+
7
+ jobs:
8
+ ci:
9
+ uses: LegionIO/.github/.github/workflows/ci.yml@main
10
+
11
+ release:
12
+ needs: ci
13
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
14
+ uses: LegionIO/.github/.github/workflows/release.yml@main
15
+ secrets:
16
+ rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ *.gem
10
+ Gemfile.lock
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --format documentation
3
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,34 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ TargetRubyVersion: 3.4
4
+
5
+ Style/Documentation:
6
+ Enabled: false
7
+
8
+ Naming/PredicateMethod:
9
+ Enabled: false
10
+
11
+ Metrics/ClassLength:
12
+ Max: 150
13
+
14
+ Metrics/MethodLength:
15
+ Max: 25
16
+
17
+ Metrics/AbcSize:
18
+ Max: 25
19
+
20
+ Metrics/ParameterLists:
21
+ Max: 8
22
+ MaxOptionalParameters: 8
23
+
24
+ Layout/HashAlignment:
25
+ EnforcedColonStyle: table
26
+ EnforcedHashRocketStyle: table
27
+
28
+ Metrics/BlockLength:
29
+ Exclude:
30
+ - 'spec/**/*'
31
+
32
+ Style/OneClassPerFile:
33
+ Exclude:
34
+ - 'spec/spec_helper.rb'
data/CLAUDE.md ADDED
@@ -0,0 +1,138 @@
1
+ # lex-semantic-priming
2
+
3
+ **Level 3 Leaf Documentation**
4
+ - **Parent**: `/Users/miverso2/rubymine/legion/extensions-agentic/CLAUDE.md`
5
+ - **Gem**: `lex-semantic-priming`
6
+ - **Version**: `0.1.0`
7
+ - **Namespace**: `Legion::Extensions::SemanticPriming`
8
+
9
+ ## Purpose
10
+
11
+ Implements spreading activation across a weighted semantic network. Nodes represent concepts; connections carry directional weights that grow stronger each time they are traversed (Hebbian reinforcement). When a seed concept is primed, activation spreads outward through connected nodes with both a per-hop decay factor and a weight-attenuated spread amount. Distinct from `lex-semantic-memory` (which stores named concept definitions with typed relations) — this stores activation states and connection weights for dynamic priming behavior.
12
+
13
+ ## Gem Info
14
+
15
+ - **Gem name**: `lex-semantic-priming`
16
+ - **License**: MIT
17
+ - **Ruby**: >= 3.4
18
+ - **No runtime dependencies** beyond the Legion framework
19
+
20
+ ## File Structure
21
+
22
+ ```
23
+ lib/legion/extensions/semantic_priming/
24
+ version.rb # VERSION = '0.1.0'
25
+ helpers/
26
+ constants.rb # limits, activation params, weight params, node types
27
+ semantic_node.rb # SemanticNode class — concept node with activation state
28
+ connection.rb # Connection class — weighted directional edge, Hebbian traversal
29
+ priming_network.rb # PrimingNetwork class — full network with BFS spreading activation
30
+ runners/
31
+ semantic_priming.rb # Runners::SemanticPriming module — all public runner methods
32
+ client.rb # Client class including Runners::SemanticPriming
33
+ ```
34
+
35
+ ## Key Constants
36
+
37
+ | Constant | Value | Purpose |
38
+ |---|---|---|
39
+ | `MAX_NODES` | 500 | Maximum nodes in the network |
40
+ | `MAX_CONNECTIONS` | 2000 | Maximum edges |
41
+ | `ACTIVATION_DECAY` | 0.05 | Per-tick activation decrease for all nodes |
42
+ | `SPREADING_FACTOR` | 0.6 | Base fraction of activation that spreads per hop |
43
+ | `PRIMING_BOOST` | 0.3 | Direct boost applied to a node when primed |
44
+ | `ACTIVATION_THRESHOLD` | 0.1 | Minimum activation to be considered active |
45
+ | `DEFAULT_WEIGHT` | 0.5 | Starting connection weight for new edges |
46
+ | `WEIGHT_GROWTH_RATE` | 0.02 | Weight increase on each traversal (Hebbian) |
47
+ | `WEIGHT_DECAY_RATE` | 0.01 | Weight decrease per decay cycle |
48
+ | `MIN_WEIGHT` | 0.05 | Floor for connection weights; pruned at or below |
49
+ | `MAX_SPREAD_DEPTH` | 3 | Maximum BFS hops during spreading activation |
50
+ | `DEPTH_DECAY_FACTOR` | 0.5 | Activation multiplier per depth level |
51
+ | `NODE_TYPES` | 7 symbols | `:concept`, `:entity`, `:action`, `:property`, `:relation`, `:event`, `:context` |
52
+
53
+ ## Helpers
54
+
55
+ ### `Helpers::SemanticNode`
56
+
57
+ Concept node with activation state and metadata.
58
+
59
+ - `initialize(id:, label:, node_type: :concept, domain: :general)` — initial activation = 0.0
60
+ - `prime!(amount = PRIMING_BOOST)` — boosts activation by amount, clamps to 1.0
61
+ - `decay!(rate = ACTIVATION_DECAY)` — decrements activation; floors at 0.0
62
+ - `access!` — records last_accessed timestamp
63
+ - `reset!` — sets activation back to 0.0
64
+ - `primed?` — activation >= 0.4
65
+ - `active?` — activation > ACTIVATION_THRESHOLD
66
+ - `activation_label` — `:dormant`, `:trace`, `:weak`, `:moderate`, `:strong`, `:peak` based on activation value
67
+
68
+ ### `Helpers::Connection`
69
+
70
+ Weighted directional edge between two nodes.
71
+
72
+ - `initialize(source_id:, target_id:, weight: DEFAULT_WEIGHT)` — assigned UUID
73
+ - `strengthen!(amount = WEIGHT_GROWTH_RATE)` — increases weight, clamps to 1.0
74
+ - `weaken!(amount = WEIGHT_DECAY_RATE)` — decreases weight; removes when at or below MIN_WEIGHT
75
+ - `traverse!(source_activation)` — calls `strengthen!` (Hebbian) and returns `spreading_amount`
76
+ - `spreading_amount(source_activation)` — `source_activation * weight * SPREADING_FACTOR`
77
+ - `weight_label` — `:negligible`, `:weak`, `:moderate`, `:strong`, `:dominant`
78
+
79
+ ### `Helpers::PrimingNetwork`
80
+
81
+ Full network with BFS spreading activation.
82
+
83
+ - `initialize` — empty nodes hash, connections hash (keyed by `source_id:target_id`)
84
+ - `add_node(label:, node_type: :concept, domain: :general)` — returns nil if at MAX_NODES
85
+ - `remove_node(node_id)` — removes node and all connections to/from it
86
+ - `connect(source_id:, target_id:, weight: DEFAULT_WEIGHT)` — creates or returns existing connection; returns nil if at MAX_CONNECTIONS
87
+ - `prime_node(node_id, amount: PRIMING_BOOST)` — calls `node.prime!`
88
+ - `spread_activation(node_id, depth: MAX_SPREAD_DEPTH, visited: {})` — recursive BFS; each level multiplied by `DEPTH_DECAY_FACTOR`; each edge traversal calls `connection.traverse!` for Hebbian strengthening
89
+ - `prime_and_spread(node_id, amount: PRIMING_BOOST, depth: MAX_SPREAD_DEPTH)` — primes seed, then spreads
90
+ - `decay_all!` — decays all node activations; weakens all connection weights (prunes at MIN_WEIGHT)
91
+ - `reset_all!` — resets all node activations to 0.0
92
+ - `find_node_by_label(label)` — substring or exact match search
93
+ - `neighbors(node_id)` — returns all outgoing connections
94
+ - `connection_between(source_id, target_id)` — direct lookup
95
+ - `primed_nodes` — nodes with `primed? == true`
96
+ - `active_nodes` — nodes with `active? == true`
97
+ - `most_primed(limit: 5)` — sorted by activation descending
98
+ - `strongest_connections(limit: 10)` — sorted by weight descending
99
+ - `average_activation` — mean activation across all nodes
100
+ - `network_density` — `connections.size.to_f / [nodes.size * (nodes.size - 1), 1].max`
101
+ - `priming_report` — summary hash including active/primed counts, average activation, density, top nodes
102
+
103
+ ## Runners
104
+
105
+ All runners are in `Runners::SemanticPriming`. Callers may pass an optional `engine:` parameter to operate on a non-default network instance.
106
+
107
+ | Runner | Parameters | Returns |
108
+ |---|---|---|
109
+ | `add_node` | `label:, node_type: :concept, domain: :general` | `{ success:, node_id:, label:, node_type: }` |
110
+ | `remove_node` | `node_id:` | `{ success: }` |
111
+ | `connect_nodes` | `source_id:, target_id:, weight: DEFAULT_WEIGHT` | `{ success:, connection_id:, source_id:, target_id:, weight: }` |
112
+ | `prime` | `node_id:, amount: PRIMING_BOOST` | `{ success:, node_id:, activation: }` |
113
+ | `prime_and_spread` | `node_id:, amount: PRIMING_BOOST, depth: MAX_SPREAD_DEPTH` | `{ success:, node_id:, activation:, spread_count: }` |
114
+ | `spread_activation` | `node_id:, depth: MAX_SPREAD_DEPTH` | `{ success:, node_id:, spread_count: }` |
115
+ | `decay` | (none) | `{ success:, active_nodes:, pruned_connections: }` |
116
+ | `reset` | (none) | `{ success: }` |
117
+ | `find_node` | `label:` | `{ success:, found:, node: }` |
118
+ | `neighbors` | `node_id:` | `{ success:, node_id:, neighbors:, count: }` |
119
+ | `primed_nodes` | (none) | `{ success:, nodes:, count: }` |
120
+ | `most_primed` | `limit: 5` | `{ success:, nodes:, count: }` |
121
+ | `priming_report` | (none) | Full `PrimingNetwork#priming_report` hash |
122
+ | `status` | (none) | Node count, connection count, average activation, density |
123
+
124
+ ## Integration Points
125
+
126
+ - **lex-semantic-memory**: semantic-memory stores named concept definitions with relational structure; semantic-priming stores transient activation state and connection weights. They are complementary — semantic-memory provides the schema; semantic-priming provides the dynamic activation surface
127
+ - **lex-dream**: association walking in the dream cycle can use `prime_and_spread` to find conceptually adjacent concepts for contradiction resolution and agenda synthesis
128
+ - **lex-tick / lex-cortex**: `prime_and_spread` or `decay` can be wired as a tick phase handler for ongoing network maintenance
129
+ - **lex-memory**: when a memory trace is retrieved, its associated concept nodes can be primed to model recency effects on conceptual accessibility
130
+
131
+ ## Development Notes
132
+
133
+ - Spreading activation is recursive, not iterative; depth is tracked via `visited` hash to prevent re-activation of already-visited nodes in the same spread call
134
+ - Each `traverse!` call on a connection triggers Hebbian strengthening — frequent traversals produce stronger connections over time, modeling use-dependent accessibility
135
+ - `DEPTH_DECAY_FACTOR = 0.5` means activation at depth 2 is 25% of the seed's activation; at depth 3, 12.5%
136
+ - `spreading_amount` = `source_activation * weight * SPREADING_FACTOR`; this triple-product means low-weight connections transmit very little even with high source activation
137
+ - `decay_all!` weaken-and-prune connections in the same pass; connections at or below `MIN_WEIGHT` are deleted
138
+ - The optional `engine:` parameter on all runners supports multiple independent networks in a single process
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :development, :test do
8
+ gem 'rspec', '~> 3.13'
9
+ gem 'rubocop', '~> 1.75'
10
+ gem 'rubocop-rspec'
11
+ end
12
+
13
+ gem 'legion-gaia', path: '../../legion-gaia'
data/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # lex-semantic-priming
2
+
3
+ Spreading activation network for LegionIO cognitive agents. Nodes represent concepts; connections carry weights that strengthen each time they are traversed (Hebbian reinforcement).
4
+
5
+ ## What It Does
6
+
7
+ `lex-semantic-priming` maintains a weighted semantic network where priming one concept causes activation to spread to related concepts with depth-attenuated strength. The network learns from use: every edge traversal during spreading activation calls `strengthen!` on that connection, gradually building stronger pathways for frequently co-activated concepts.
8
+
9
+ - **Nodes**: typed concept nodes (`:concept`, `:entity`, `:action`, `:property`, `:relation`, `:event`, `:context`)
10
+ - **Connections**: directional weighted edges; weights grow on traversal (Hebbian), decay over time
11
+ - **Spreading activation**: BFS from a seed node across outgoing connections; activation decays by `DEPTH_DECAY_FACTOR` (0.5) per hop and by `weight * SPREADING_FACTOR` (0.6) per edge
12
+ - **Decay**: all node activations and connection weights decay each tick; connections pruned at MIN_WEIGHT (0.05)
13
+ - **Primed nodes**: activation >= 0.4; active nodes: activation > 0.1
14
+
15
+ ## Usage
16
+
17
+ ```ruby
18
+ require 'legion/extensions/semantic_priming'
19
+
20
+ client = Legion::Extensions::SemanticPriming::Client.new
21
+
22
+ # Add concept nodes
23
+ ruby_id = client.add_node(label: 'ruby', node_type: :concept, domain: :programming)[:node_id]
24
+ oop_id = client.add_node(label: 'object_oriented', node_type: :concept, domain: :programming)[:node_id]
25
+ class_id = client.add_node(label: 'class', node_type: :concept, domain: :programming)[:node_id]
26
+
27
+ # Connect them
28
+ client.connect_nodes(source_id: ruby_id, target_id: oop_id)
29
+ client.connect_nodes(source_id: oop_id, target_id: class_id)
30
+
31
+ # Prime a seed and spread activation
32
+ result = client.prime_and_spread(node_id: ruby_id)
33
+ # => { success: true, node_id: ..., activation: 0.3, spread_count: 2 }
34
+
35
+ # See which nodes are primed (activation >= 0.4)
36
+ client.primed_nodes
37
+ # => { success: true, nodes: [...], count: 1 }
38
+
39
+ # Most activated nodes
40
+ client.most_primed(limit: 5)
41
+ # => { success: true, nodes: [{ label: 'ruby', activation: 0.3, ... }, ...], count: ... }
42
+
43
+ # Per-tick decay (call each cognitive cycle)
44
+ client.decay
45
+ # => { success: true, active_nodes: 2, pruned_connections: 0 }
46
+
47
+ # Network summary
48
+ client.priming_report
49
+ # => { node_count:, connection_count:, active_count:, primed_count:, average_activation:, density:, ... }
50
+ ```
51
+
52
+ ## Development
53
+
54
+ ```bash
55
+ bundle install
56
+ bundle exec rspec
57
+ bundle exec rubocop
58
+ ```
59
+
60
+ ## License
61
+
62
+ MIT
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/semantic_priming/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-semantic-priming'
7
+ spec.version = Legion::Extensions::SemanticPriming::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'Semantic priming and spreading activation network for LegionIO'
12
+ spec.description = 'Models spreading activation in semantic networks - priming one concept activates related ' \
13
+ 'concepts with distance-based decay for rapid associative retrieval.'
14
+ spec.homepage = 'https://github.com/LegionIO/lex-semantic-priming'
15
+ spec.license = 'MIT'
16
+
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-priming'
21
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-semantic-priming/blob/master/README.md'
22
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-semantic-priming/blob/master/CHANGELOG.md'
23
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-semantic-priming/issues'
24
+ spec.metadata['rubygems_mfa_required'] = 'true'
25
+
26
+ spec.files = Dir.chdir(__dir__) do
27
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
28
+ end
29
+ spec.require_paths = ['lib']
30
+ spec.add_development_dependency 'legion-gaia'
31
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module SemanticPriming
6
+ class Client
7
+ include Runners::SemanticPriming
8
+
9
+ def initialize(engine: nil)
10
+ @default_engine = engine || Helpers::PrimingNetwork.new
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module SemanticPriming
8
+ module Helpers
9
+ class Connection
10
+ include Constants
11
+
12
+ attr_reader :id, :source_id, :target_id, :weight, :traversal_count, :created_at
13
+
14
+ def initialize(source_id:, target_id:, weight: DEFAULT_WEIGHT)
15
+ @id = SecureRandom.uuid
16
+ @source_id = source_id
17
+ @target_id = target_id
18
+ @weight = weight.to_f.clamp(MIN_WEIGHT, 1.0).round(10)
19
+ @traversal_count = 0
20
+ @created_at = Time.now.utc
21
+ end
22
+
23
+ def strengthen!(amount: WEIGHT_GROWTH_RATE)
24
+ @weight = (@weight + amount).clamp(MIN_WEIGHT, 1.0).round(10)
25
+ self
26
+ end
27
+
28
+ def weaken!(amount: WEIGHT_DECAY_RATE)
29
+ @weight = (@weight - amount).clamp(MIN_WEIGHT, 1.0).round(10)
30
+ self
31
+ end
32
+
33
+ def traverse!
34
+ @traversal_count += 1
35
+ strengthen!(amount: WEIGHT_GROWTH_RATE)
36
+ self
37
+ end
38
+
39
+ def strong?
40
+ @weight >= 0.7
41
+ end
42
+
43
+ def weak?
44
+ @weight <= 0.2
45
+ end
46
+
47
+ def spreading_amount(source_activation)
48
+ (source_activation * @weight * SPREADING_FACTOR).round(10)
49
+ end
50
+
51
+ def weight_label
52
+ match = WEIGHT_LABELS.find { |range, _| range.cover?(@weight) }
53
+ match ? match.last : :very_weak
54
+ end
55
+
56
+ def to_h
57
+ {
58
+ id: @id,
59
+ source_id: @source_id,
60
+ target_id: @target_id,
61
+ weight: @weight,
62
+ weight_label: weight_label,
63
+ strong: strong?,
64
+ weak: weak?,
65
+ traversal_count: @traversal_count,
66
+ created_at: @created_at
67
+ }
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module SemanticPriming
6
+ module Helpers
7
+ module Constants
8
+ # Network limits
9
+ MAX_NODES = 500
10
+ MAX_CONNECTIONS = 2000
11
+
12
+ # Activation dynamics
13
+ DEFAULT_ACTIVATION = 0.0
14
+ RESTING_ACTIVATION = 0.0
15
+ MAX_ACTIVATION = 1.0
16
+ ACTIVATION_DECAY = 0.05
17
+ SPREADING_FACTOR = 0.6
18
+ PRIMING_BOOST = 0.3
19
+ ACTIVATION_THRESHOLD = 0.1
20
+
21
+ # Connection properties
22
+ DEFAULT_WEIGHT = 0.5
23
+ WEIGHT_GROWTH_RATE = 0.02
24
+ WEIGHT_DECAY_RATE = 0.01
25
+ MIN_WEIGHT = 0.05
26
+
27
+ # Spreading activation
28
+ MAX_SPREAD_DEPTH = 3
29
+ DEPTH_DECAY_FACTOR = 0.5
30
+
31
+ # Node types
32
+ NODE_TYPES = %i[concept category feature relation action emotion context].freeze
33
+
34
+ # Activation labels
35
+ ACTIVATION_LABELS = {
36
+ (0.8..) => :highly_primed,
37
+ (0.6...0.8) => :primed,
38
+ (0.4...0.6) => :partially_primed,
39
+ (0.2...0.4) => :weakly_primed,
40
+ (..0.2) => :unprimed
41
+ }.freeze
42
+
43
+ # Connection strength labels
44
+ WEIGHT_LABELS = {
45
+ (0.8..) => :very_strong,
46
+ (0.6...0.8) => :strong,
47
+ (0.4...0.6) => :moderate,
48
+ (0.2...0.4) => :weak,
49
+ (..0.2) => :very_weak
50
+ }.freeze
51
+
52
+ # Priming effect labels
53
+ PRIMING_LABELS = {
54
+ (0.8..) => :massive,
55
+ (0.6...0.8) => :strong,
56
+ (0.4...0.6) => :moderate,
57
+ (0.2...0.4) => :mild,
58
+ (..0.2) => :negligible
59
+ }.freeze
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module SemanticPriming
6
+ module Helpers
7
+ class PrimingNetwork
8
+ include Constants
9
+
10
+ def initialize
11
+ @nodes = {}
12
+ @connections = {}
13
+ @adjacency = Hash.new { |h, k| h[k] = [] }
14
+ end
15
+
16
+ def add_node(label:, node_type: :concept)
17
+ prune_nodes_if_needed
18
+ node = SemanticNode.new(label: label, node_type: node_type)
19
+ @nodes[node.id] = node
20
+ node
21
+ end
22
+
23
+ def remove_node(node_id:)
24
+ node = @nodes.delete(node_id)
25
+ return nil unless node
26
+
27
+ @adjacency.delete(node_id)
28
+ @adjacency.each_value { |list| list.reject! { |cid| connection_involves?(cid, node_id) } }
29
+ @connections.reject! { |_, c| c.source_id == node_id || c.target_id == node_id }
30
+ node
31
+ end
32
+
33
+ def connect(source_id:, target_id:, weight: DEFAULT_WEIGHT)
34
+ return nil unless @nodes[source_id] && @nodes[target_id]
35
+ return nil if source_id == target_id
36
+
37
+ prune_connections_if_needed
38
+ conn = Connection.new(source_id: source_id, target_id: target_id, weight: weight)
39
+ @connections[conn.id] = conn
40
+ @adjacency[source_id] << conn.id
41
+ @adjacency[target_id] << conn.id
42
+ conn
43
+ end
44
+
45
+ def prime_node(node_id:, amount: PRIMING_BOOST)
46
+ node = @nodes[node_id]
47
+ return nil unless node
48
+
49
+ node.prime!(amount: amount)
50
+ node
51
+ end
52
+
53
+ def spread_activation(source_id:, depth: MAX_SPREAD_DEPTH)
54
+ source = @nodes[source_id]
55
+ return nil unless source
56
+
57
+ activated = {}
58
+ spread_recursive(source_id, source.activation, depth, 0, activated)
59
+ activated.map { |nid, amount| { node_id: nid, label: @nodes[nid]&.label, activation_added: amount } }
60
+ end
61
+
62
+ def prime_and_spread(node_id:, amount: PRIMING_BOOST, depth: MAX_SPREAD_DEPTH)
63
+ node = prime_node(node_id: node_id, amount: amount)
64
+ return nil unless node
65
+
66
+ node.access!
67
+ spread = spread_activation(source_id: node_id, depth: depth)
68
+ { primed_node: node.to_h, spread: spread }
69
+ end
70
+
71
+ def decay_all!
72
+ @nodes.each_value(&:decay!)
73
+ @connections.each_value { |c| c.weaken!(amount: WEIGHT_DECAY_RATE) }
74
+ prune_weak_connections
75
+ { nodes_decayed: @nodes.size, connections_remaining: @connections.size }
76
+ end
77
+
78
+ def reset_all! = @nodes.each_value(&:reset!) && { nodes_reset: @nodes.size }
79
+ def find_node_by_label(label:) = @nodes.values.find { |n| n.label == label.to_s }
80
+
81
+ def neighbors(node_id:)
82
+ conn_ids = @adjacency[node_id] || []
83
+ conn_ids.filter_map do |cid|
84
+ conn = @connections[cid]
85
+ next unless conn
86
+
87
+ other_id = conn.source_id == node_id ? conn.target_id : conn.source_id
88
+ @nodes[other_id]
89
+ end
90
+ end
91
+
92
+ def connection_between(source_id:, target_id:)
93
+ @connections.values.find do |c|
94
+ (c.source_id == source_id && c.target_id == target_id) ||
95
+ (c.source_id == target_id && c.target_id == source_id)
96
+ end
97
+ end
98
+
99
+ def primed_nodes = @nodes.values.select(&:primed?)
100
+ def active_nodes = @nodes.values.select(&:active?)
101
+ def most_primed(limit: 5) = @nodes.values.sort_by { |n| -n.activation }.first(limit)
102
+ def strongest_connections(limit: 5) = @connections.values.sort_by { |c| -c.weight }.first(limit)
103
+
104
+ def average_activation
105
+ return DEFAULT_ACTIVATION if @nodes.empty?
106
+
107
+ vals = @nodes.values.map(&:activation)
108
+ (vals.sum / vals.size).round(10)
109
+ end
110
+
111
+ def average_connection_weight
112
+ return DEFAULT_WEIGHT if @connections.empty?
113
+
114
+ vals = @connections.values.map(&:weight)
115
+ (vals.sum / vals.size).round(10)
116
+ end
117
+
118
+ def network_density
119
+ return 0.0 if @nodes.size < 2
120
+
121
+ (@connections.size.to_f / (@nodes.size * (@nodes.size - 1) / 2)).round(10)
122
+ end
123
+
124
+ def priming_report
125
+ to_h.merge(
126
+ average_weight: average_connection_weight,
127
+ most_primed: most_primed(limit: 3).map(&:to_h),
128
+ strongest_connections: strongest_connections(limit: 3).map(&:to_h)
129
+ )
130
+ end
131
+
132
+ def to_h
133
+ {
134
+ total_nodes: @nodes.size,
135
+ total_connections: @connections.size,
136
+ primed_count: primed_nodes.size,
137
+ active_count: active_nodes.size,
138
+ average_activation: average_activation,
139
+ network_density: network_density
140
+ }
141
+ end
142
+
143
+ private
144
+
145
+ def spread_recursive(node_id, activation, max_depth, current_depth, activated)
146
+ return if current_depth >= max_depth || activation < ACTIVATION_THRESHOLD
147
+
148
+ each_neighbor(node_id) do |conn, other_id, target_node|
149
+ next if activated.key?(other_id)
150
+
151
+ amount = (conn.spreading_amount(activation) * (DEPTH_DECAY_FACTOR**current_depth)).round(10)
152
+ next if amount < ACTIVATION_THRESHOLD
153
+
154
+ conn.traverse!
155
+ target_node.prime!(amount: amount)
156
+ activated[other_id] = amount
157
+ spread_recursive(other_id, amount, max_depth, current_depth + 1, activated)
158
+ end
159
+ end
160
+
161
+ def each_neighbor(node_id)
162
+ (@adjacency[node_id] || []).each do |cid|
163
+ conn = @connections[cid]
164
+ next unless conn
165
+
166
+ other_id = conn.source_id == node_id ? conn.target_id : conn.source_id
167
+ target_node = @nodes[other_id]
168
+ yield conn, other_id, target_node if target_node
169
+ end
170
+ end
171
+
172
+ def connection_involves?(cid, nid)
173
+ (c = @connections[cid]) && (c.source_id == nid || c.target_id == nid)
174
+ end
175
+
176
+ def prune_nodes_if_needed
177
+ return if @nodes.size < MAX_NODES
178
+
179
+ remove_node(node_id: @nodes.values.min_by(&:activation).id)
180
+ end
181
+
182
+ def prune_connections_if_needed
183
+ return if @connections.size < MAX_CONNECTIONS
184
+
185
+ remove_connection(@connections.values.min_by(&:weight).id)
186
+ end
187
+
188
+ def prune_weak_connections
189
+ @connections.each { |id, c| remove_connection(id) if c.weight <= MIN_WEIGHT }
190
+ end
191
+
192
+ def remove_connection(conn_id)
193
+ return unless (conn = @connections.delete(conn_id))
194
+
195
+ @adjacency[conn.source_id]&.delete(conn_id)
196
+ @adjacency[conn.target_id]&.delete(conn_id)
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module SemanticPriming
8
+ module Helpers
9
+ class SemanticNode
10
+ include Constants
11
+
12
+ attr_reader :id, :label, :node_type, :activation, :prime_count,
13
+ :access_count, :created_at
14
+
15
+ def initialize(label:, node_type: :concept, activation: DEFAULT_ACTIVATION)
16
+ @id = SecureRandom.uuid
17
+ @label = label.to_s
18
+ @node_type = node_type.to_sym
19
+ @activation = activation.to_f.clamp(0.0, MAX_ACTIVATION).round(10)
20
+ @prime_count = 0
21
+ @access_count = 0
22
+ @created_at = Time.now.utc
23
+ end
24
+
25
+ def prime!(amount: PRIMING_BOOST)
26
+ @activation = (@activation + amount).clamp(0.0, MAX_ACTIVATION).round(10)
27
+ @prime_count += 1
28
+ self
29
+ end
30
+
31
+ def decay!
32
+ @activation = (@activation - ACTIVATION_DECAY).clamp(0.0, MAX_ACTIVATION).round(10)
33
+ self
34
+ end
35
+
36
+ def access!
37
+ @access_count += 1
38
+ self
39
+ end
40
+
41
+ def reset!
42
+ @activation = RESTING_ACTIVATION
43
+ self
44
+ end
45
+
46
+ def primed?
47
+ @activation >= 0.4
48
+ end
49
+
50
+ def active?
51
+ @activation > ACTIVATION_THRESHOLD
52
+ end
53
+
54
+ def activation_label
55
+ match = ACTIVATION_LABELS.find { |range, _| range.cover?(@activation) }
56
+ match ? match.last : :unprimed
57
+ end
58
+
59
+ def to_h
60
+ {
61
+ id: @id,
62
+ label: @label,
63
+ node_type: @node_type,
64
+ activation: @activation,
65
+ activation_label: activation_label,
66
+ primed: primed?,
67
+ active: active?,
68
+ prime_count: @prime_count,
69
+ access_count: @access_count,
70
+ created_at: @created_at
71
+ }
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module SemanticPriming
6
+ module Runners
7
+ module SemanticPriming
8
+ include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
9
+
10
+ def add_node(label:, node_type: :concept, engine: nil, **)
11
+ eng = engine || default_engine
12
+ node = eng.add_node(label: label, node_type: node_type)
13
+ { success: true, node: node.to_h }
14
+ end
15
+
16
+ def remove_node(node_id:, engine: nil, **)
17
+ eng = engine || default_engine
18
+ node = eng.remove_node(node_id: node_id)
19
+ return { success: false, error: 'node not found' } unless node
20
+
21
+ { success: true, removed: node.to_h }
22
+ end
23
+
24
+ def connect_nodes(source_id:, target_id:, weight: nil, engine: nil, **)
25
+ eng = engine || default_engine
26
+ w = weight || Helpers::Constants::DEFAULT_WEIGHT
27
+ conn = eng.connect(source_id: source_id, target_id: target_id, weight: w)
28
+ return { success: false, error: 'invalid nodes or self-connection' } unless conn
29
+
30
+ { success: true, connection: conn.to_h }
31
+ end
32
+
33
+ def prime(node_id:, amount: nil, engine: nil, **)
34
+ eng = engine || default_engine
35
+ amt = amount || Helpers::Constants::PRIMING_BOOST
36
+ node = eng.prime_node(node_id: node_id, amount: amt)
37
+ return { success: false, error: 'node not found' } unless node
38
+
39
+ { success: true, node: node.to_h }
40
+ end
41
+
42
+ def prime_and_spread(node_id:, amount: nil, depth: nil, engine: nil, **)
43
+ eng = engine || default_engine
44
+ amt = amount || Helpers::Constants::PRIMING_BOOST
45
+ d = depth || Helpers::Constants::MAX_SPREAD_DEPTH
46
+ result = eng.prime_and_spread(node_id: node_id, amount: amt, depth: d)
47
+ return { success: false, error: 'node not found' } unless result
48
+
49
+ { success: true, **result }
50
+ end
51
+
52
+ def spread_activation(source_id:, depth: nil, engine: nil, **)
53
+ eng = engine || default_engine
54
+ d = depth || Helpers::Constants::MAX_SPREAD_DEPTH
55
+ result = eng.spread_activation(source_id: source_id, depth: d)
56
+ return { success: false, error: 'node not found' } unless result
57
+
58
+ { success: true, activated: result }
59
+ end
60
+
61
+ def decay(engine: nil, **)
62
+ eng = engine || default_engine
63
+ result = eng.decay_all!
64
+ { success: true, **result }
65
+ end
66
+
67
+ def reset(engine: nil, **)
68
+ eng = engine || default_engine
69
+ result = eng.reset_all!
70
+ { success: true, **result }
71
+ end
72
+
73
+ def find_node(label:, engine: nil, **)
74
+ eng = engine || default_engine
75
+ node = eng.find_node_by_label(label: label)
76
+ return { success: false, error: 'node not found' } unless node
77
+
78
+ { success: true, node: node.to_h }
79
+ end
80
+
81
+ def neighbors(node_id:, engine: nil, **)
82
+ eng = engine || default_engine
83
+ nodes = eng.neighbors(node_id: node_id)
84
+ { success: true, neighbors: nodes.map(&:to_h) }
85
+ end
86
+
87
+ def primed_nodes(engine: nil, **)
88
+ eng = engine || default_engine
89
+ { success: true, nodes: eng.primed_nodes.map(&:to_h) }
90
+ end
91
+
92
+ def most_primed(limit: 5, engine: nil, **)
93
+ eng = engine || default_engine
94
+ { success: true, nodes: eng.most_primed(limit: limit).map(&:to_h) }
95
+ end
96
+
97
+ def priming_report(engine: nil, **)
98
+ eng = engine || default_engine
99
+ { success: true, report: eng.priming_report }
100
+ end
101
+
102
+ def status(engine: nil, **)
103
+ eng = engine || default_engine
104
+ { success: true, **eng.to_h }
105
+ end
106
+
107
+ private
108
+
109
+ def default_engine
110
+ @default_engine ||= Helpers::PrimingNetwork.new
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module SemanticPriming
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'semantic_priming/version'
4
+ require_relative 'semantic_priming/helpers/constants'
5
+ require_relative 'semantic_priming/helpers/semantic_node'
6
+ require_relative 'semantic_priming/helpers/connection'
7
+ require_relative 'semantic_priming/helpers/priming_network'
8
+ require_relative 'semantic_priming/runners/semantic_priming'
9
+ require_relative 'semantic_priming/client'
10
+
11
+ module Legion
12
+ module Extensions
13
+ module SemanticPriming
14
+ extend Legion::Extensions::Core if defined?(Legion::Extensions::Core)
15
+ end
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-semantic-priming
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: Models spreading activation in semantic networks - priming one concept
27
+ activates related concepts with distance-based decay for rapid associative retrieval.
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".github/workflows/ci.yml"
35
+ - ".gitignore"
36
+ - ".rspec"
37
+ - ".rubocop.yml"
38
+ - CLAUDE.md
39
+ - Gemfile
40
+ - README.md
41
+ - lex-semantic-priming.gemspec
42
+ - lib/legion/extensions/semantic_priming.rb
43
+ - lib/legion/extensions/semantic_priming/client.rb
44
+ - lib/legion/extensions/semantic_priming/helpers/connection.rb
45
+ - lib/legion/extensions/semantic_priming/helpers/constants.rb
46
+ - lib/legion/extensions/semantic_priming/helpers/priming_network.rb
47
+ - lib/legion/extensions/semantic_priming/helpers/semantic_node.rb
48
+ - lib/legion/extensions/semantic_priming/runners/semantic_priming.rb
49
+ - lib/legion/extensions/semantic_priming/version.rb
50
+ homepage: https://github.com/LegionIO/lex-semantic-priming
51
+ licenses:
52
+ - MIT
53
+ metadata:
54
+ homepage_uri: https://github.com/LegionIO/lex-semantic-priming
55
+ source_code_uri: https://github.com/LegionIO/lex-semantic-priming
56
+ documentation_uri: https://github.com/LegionIO/lex-semantic-priming/blob/master/README.md
57
+ changelog_uri: https://github.com/LegionIO/lex-semantic-priming/blob/master/CHANGELOG.md
58
+ bug_tracker_uri: https://github.com/LegionIO/lex-semantic-priming/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: Semantic priming and spreading activation network for LegionIO
77
+ test_files: []