lex-frame-semantics 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-frame-semantics.gemspec +29 -0
- data/lib/legion/extensions/frame_semantics/helpers/client.rb +19 -0
- data/lib/legion/extensions/frame_semantics/helpers/constants.rb +28 -0
- data/lib/legion/extensions/frame_semantics/helpers/frame.rb +105 -0
- data/lib/legion/extensions/frame_semantics/helpers/frame_engine.rb +135 -0
- data/lib/legion/extensions/frame_semantics/helpers/frame_instance.rb +47 -0
- data/lib/legion/extensions/frame_semantics/runners/frame_semantics.rb +102 -0
- data/lib/legion/extensions/frame_semantics/version.rb +9 -0
- data/lib/legion/extensions/frame_semantics.rb +17 -0
- data/spec/legion/extensions/frame_semantics/helpers/frame_engine_spec.rb +227 -0
- data/spec/legion/extensions/frame_semantics/helpers/frame_instance_spec.rb +83 -0
- data/spec/legion/extensions/frame_semantics/helpers/frame_spec.rb +213 -0
- data/spec/legion/extensions/frame_semantics/runners/frame_semantics_spec.rb +155 -0
- data/spec/spec_helper.rb +20 -0
- metadata +76 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a2fd7637ea32eaf9de85020e859dfd65525545fcde99f19b1ec0e6f83caa248e
|
|
4
|
+
data.tar.gz: 7127f8ba47ae7f06f339c62586bebbe44389d338a3a3a990faca109357517451
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c979ec6a48383fa8b5b1c34af874d011ee9cfe1ffd7f6c5a7eb91921f88643474da0ae9ee860d048c414c80a6d5b4d84a39fdd309f01115356b3842f323256b7
|
|
7
|
+
data.tar.gz: c2516c54bac8155ac8209c5b3579eb393b3147a55d686a516c57bd285ab647ce933bb4b73facb6de5a18d1529748bf8db6d7e32574741cf5ae8ea2596a7dfbaf
|
data/Gemfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/frame_semantics/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-frame-semantics'
|
|
7
|
+
spec.version = Legion::Extensions::FrameSemantics::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Frame Semantics'
|
|
12
|
+
spec.description = "Fillmore's Frame Semantics engine for LegionIO — conceptual frame activation, slot filling, and instance creation"
|
|
13
|
+
spec.homepage = 'https://github.com/LegionIO/lex-frame-semantics'
|
|
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-frame-semantics'
|
|
19
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-frame-semantics'
|
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-frame-semantics'
|
|
21
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-frame-semantics/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-frame-semantics.gemspec Gemfile]
|
|
26
|
+
end
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
spec.add_development_dependency 'legion-gaia'
|
|
29
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module FrameSemantics
|
|
6
|
+
module Helpers
|
|
7
|
+
class Client
|
|
8
|
+
include Runners::FrameSemantics
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def engine
|
|
13
|
+
@engine ||= FrameEngine.new
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module FrameSemantics
|
|
6
|
+
module Helpers
|
|
7
|
+
SLOT_TYPES = %i[core peripheral extra_thematic].freeze
|
|
8
|
+
FRAME_RELATIONS = %i[inherits_from is_inherited_by uses is_used_by subframe_of has_subframe].freeze
|
|
9
|
+
ACTIVATION_LABELS = {
|
|
10
|
+
(0.8..) => :dominant,
|
|
11
|
+
(0.6...0.8) => :active,
|
|
12
|
+
(0.4...0.6) => :primed,
|
|
13
|
+
(0.2...0.4) => :latent,
|
|
14
|
+
(..0.2) => :inactive
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
MAX_FRAMES = 150
|
|
18
|
+
MAX_INSTANCES = 500
|
|
19
|
+
MAX_HISTORY = 500
|
|
20
|
+
DEFAULT_ACTIVATION = 0.3
|
|
21
|
+
ACTIVATION_BOOST = 0.15
|
|
22
|
+
ACTIVATION_DECAY = 0.03
|
|
23
|
+
SLOT_FILL_BOOST = 0.1
|
|
24
|
+
COMPLETION_THRESHOLD = 0.7
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module FrameSemantics
|
|
8
|
+
module Helpers
|
|
9
|
+
class Frame
|
|
10
|
+
attr_reader :id, :name, :domain, :slots, :relations, :activation, :activation_count, :created_at
|
|
11
|
+
|
|
12
|
+
def initialize(name:, domain:)
|
|
13
|
+
@id = SecureRandom.uuid
|
|
14
|
+
@name = name
|
|
15
|
+
@domain = domain
|
|
16
|
+
@slots = {}
|
|
17
|
+
@relations = []
|
|
18
|
+
@activation = DEFAULT_ACTIVATION
|
|
19
|
+
@activation_count = 0
|
|
20
|
+
@created_at = Time.now.utc
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add_slot(name:, slot_type: :core, required: true)
|
|
24
|
+
@slots[name] = { type: slot_type, filler: nil, required: required }
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def fill_slot(name:, filler:)
|
|
29
|
+
return false unless @slots.key?(name)
|
|
30
|
+
|
|
31
|
+
@slots[name][:filler] = filler
|
|
32
|
+
@activation = [@activation + SLOT_FILL_BOOST, 1.0].min
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def clear_slot(name:)
|
|
37
|
+
return false unless @slots.key?(name)
|
|
38
|
+
|
|
39
|
+
@slots[name][:filler] = nil
|
|
40
|
+
true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def core_slots
|
|
44
|
+
@slots.select { |_k, v| v[:type] == :core }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def filled_slots
|
|
48
|
+
@slots.reject { |_k, v| v[:filler].nil? }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def completion_ratio
|
|
52
|
+
cs = core_slots
|
|
53
|
+
return 0.0 if cs.empty?
|
|
54
|
+
|
|
55
|
+
filled_core = cs.count { |_k, v| !v[:filler].nil? }
|
|
56
|
+
filled_core.to_f / cs.size
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def complete?
|
|
60
|
+
completion_ratio >= COMPLETION_THRESHOLD
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def activate!
|
|
64
|
+
@activation = [@activation + ACTIVATION_BOOST, 1.0].min
|
|
65
|
+
@activation_count += 1
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def decay!
|
|
70
|
+
@activation = [@activation - ACTIVATION_DECAY, 0.0].max
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def activation_label
|
|
75
|
+
ACTIVATION_LABELS.each do |range, label|
|
|
76
|
+
return label if range.cover?(@activation)
|
|
77
|
+
end
|
|
78
|
+
:inactive
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def add_relation(relation:, target_frame_id:)
|
|
82
|
+
@relations << { relation: relation, target_frame_id: target_frame_id }
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def to_h
|
|
87
|
+
{
|
|
88
|
+
id: @id,
|
|
89
|
+
name: @name,
|
|
90
|
+
domain: @domain,
|
|
91
|
+
slots: @slots,
|
|
92
|
+
relations: @relations,
|
|
93
|
+
activation: @activation,
|
|
94
|
+
activation_label: activation_label,
|
|
95
|
+
activation_count: @activation_count,
|
|
96
|
+
completion_ratio: completion_ratio,
|
|
97
|
+
complete: complete?,
|
|
98
|
+
created_at: @created_at
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module FrameSemantics
|
|
6
|
+
module Helpers
|
|
7
|
+
class FrameEngine
|
|
8
|
+
def initialize
|
|
9
|
+
@frames = {}
|
|
10
|
+
@instances = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create_frame(name:, domain:, slots: {})
|
|
14
|
+
frame = Frame.new(name: name, domain: domain)
|
|
15
|
+
slots.each do |slot_name, opts|
|
|
16
|
+
frame.add_slot(
|
|
17
|
+
name: slot_name,
|
|
18
|
+
slot_type: opts.fetch(:type, :core),
|
|
19
|
+
required: opts.fetch(:required, true)
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
prune_frames_if_full
|
|
23
|
+
@frames[frame.id] = frame
|
|
24
|
+
frame
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def define_slot(frame_id:, name:, slot_type: :core, required: true)
|
|
28
|
+
frame = @frames[frame_id]
|
|
29
|
+
return nil unless frame
|
|
30
|
+
|
|
31
|
+
frame.add_slot(name: name, slot_type: slot_type, required: required)
|
|
32
|
+
frame
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def fill_slot(frame_id:, slot_name:, filler:)
|
|
36
|
+
frame = @frames[frame_id]
|
|
37
|
+
return false unless frame
|
|
38
|
+
|
|
39
|
+
result = frame.fill_slot(name: slot_name, filler: filler)
|
|
40
|
+
frame.activate! if result
|
|
41
|
+
result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def instantiate_frame(frame_id:, context:, confidence: 0.7)
|
|
45
|
+
frame = @frames[frame_id]
|
|
46
|
+
return nil unless frame
|
|
47
|
+
|
|
48
|
+
fillers = frame.slots.transform_values { |v| v[:filler] }
|
|
49
|
+
instance = FrameInstance.new(
|
|
50
|
+
frame_id: frame_id,
|
|
51
|
+
frame_name: frame.name,
|
|
52
|
+
slot_fillers: fillers,
|
|
53
|
+
context: context,
|
|
54
|
+
confidence: confidence
|
|
55
|
+
)
|
|
56
|
+
@instances << instance
|
|
57
|
+
@instances.shift while @instances.size > MAX_INSTANCES
|
|
58
|
+
instance
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def add_frame_relation(frame_id:, relation:, target_frame_id:)
|
|
62
|
+
frame = @frames[frame_id]
|
|
63
|
+
return false unless frame
|
|
64
|
+
return false unless FRAME_RELATIONS.include?(relation)
|
|
65
|
+
|
|
66
|
+
frame.add_relation(relation: relation, target_frame_id: target_frame_id)
|
|
67
|
+
true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def activate_frame(frame_id:)
|
|
71
|
+
frame = @frames[frame_id]
|
|
72
|
+
return false unless frame
|
|
73
|
+
|
|
74
|
+
frame.activate!
|
|
75
|
+
true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def active_frames
|
|
79
|
+
@frames.values.select { |f| f.activation > 0.5 }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def frames_by_domain(domain:)
|
|
83
|
+
@frames.values.select { |f| f.domain == domain }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def related_frames(frame_id:)
|
|
87
|
+
frame = @frames[frame_id]
|
|
88
|
+
return [] unless frame
|
|
89
|
+
|
|
90
|
+
frame.relations.filter_map { |rel| @frames[rel[:target_frame_id]] }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def most_activated(limit: 5)
|
|
94
|
+
@frames.values.sort_by { |f| -f.activation }.first(limit)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def instances_for_frame(frame_id:)
|
|
98
|
+
@instances.select { |i| i.frame_id == frame_id }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def complete_frames
|
|
102
|
+
@frames.values.select(&:complete?)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def decay_all
|
|
106
|
+
@frames.each_value(&:decay!)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def prune_inactive
|
|
110
|
+
@frames.reject! { |_id, f| f.activation <= 0.05 }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def to_h
|
|
114
|
+
{
|
|
115
|
+
frame_count: @frames.size,
|
|
116
|
+
instance_count: @instances.size,
|
|
117
|
+
active_count: active_frames.size,
|
|
118
|
+
complete_count: complete_frames.size,
|
|
119
|
+
domains: @frames.values.map(&:domain).uniq.sort
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def prune_frames_if_full
|
|
126
|
+
return unless @frames.size >= MAX_FRAMES
|
|
127
|
+
|
|
128
|
+
oldest = @frames.values.min_by(&:activation)
|
|
129
|
+
@frames.delete(oldest.id) if oldest
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module FrameSemantics
|
|
8
|
+
module Helpers
|
|
9
|
+
class FrameInstance
|
|
10
|
+
attr_reader :id, :frame_id, :frame_name, :slot_fillers, :context, :confidence, :created_at
|
|
11
|
+
|
|
12
|
+
def initialize(frame_id:, frame_name:, slot_fillers:, context:, confidence: 0.7)
|
|
13
|
+
@id = SecureRandom.uuid
|
|
14
|
+
@frame_id = frame_id
|
|
15
|
+
@frame_name = frame_name
|
|
16
|
+
@slot_fillers = slot_fillers.dup
|
|
17
|
+
@context = context
|
|
18
|
+
@confidence = confidence
|
|
19
|
+
@created_at = Time.now.utc
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def complete?
|
|
23
|
+
filled_count.positive?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def filled_count
|
|
27
|
+
@slot_fillers.count { |_k, v| !v.nil? }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_h
|
|
31
|
+
{
|
|
32
|
+
id: @id,
|
|
33
|
+
frame_id: @frame_id,
|
|
34
|
+
frame_name: @frame_name,
|
|
35
|
+
slot_fillers: @slot_fillers,
|
|
36
|
+
context: @context,
|
|
37
|
+
confidence: @confidence,
|
|
38
|
+
filled_count: filled_count,
|
|
39
|
+
complete: complete?,
|
|
40
|
+
created_at: @created_at
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module FrameSemantics
|
|
6
|
+
module Runners
|
|
7
|
+
module FrameSemantics
|
|
8
|
+
def create_semantic_frame(name:, domain:, **)
|
|
9
|
+
frame = engine.create_frame(name: name, domain: domain)
|
|
10
|
+
Legion::Logging.debug "[frame_semantics] created frame name=#{name} domain=#{domain} id=#{frame.id[0..7]}"
|
|
11
|
+
{ frame_id: frame.id, name: frame.name, domain: frame.domain, created: true }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def define_frame_slot(frame_id:, name:, slot_type: :core, required: true, **)
|
|
15
|
+
frame = engine.define_slot(frame_id: frame_id, name: name, slot_type: slot_type, required: required)
|
|
16
|
+
if frame
|
|
17
|
+
Legion::Logging.debug "[frame_semantics] defined slot #{name} on frame #{frame_id[0..7]}"
|
|
18
|
+
{ defined: true, frame_id: frame_id, slot_name: name, slot_type: slot_type }
|
|
19
|
+
else
|
|
20
|
+
{ defined: false, reason: :frame_not_found }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fill_frame_slot(frame_id:, slot_name:, filler:, **)
|
|
25
|
+
result = engine.fill_slot(frame_id: frame_id, slot_name: slot_name, filler: filler)
|
|
26
|
+
if result
|
|
27
|
+
Legion::Logging.debug "[frame_semantics] filled slot #{slot_name} on frame #{frame_id[0..7]}"
|
|
28
|
+
{ filled: true, frame_id: frame_id, slot_name: slot_name, filler: filler }
|
|
29
|
+
else
|
|
30
|
+
{ filled: false, reason: :slot_or_frame_not_found }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def instantiate_semantic_frame(frame_id:, context:, confidence: 0.7, **)
|
|
35
|
+
instance = engine.instantiate_frame(frame_id: frame_id, context: context, confidence: confidence)
|
|
36
|
+
if instance
|
|
37
|
+
Legion::Logging.debug "[frame_semantics] instantiated frame #{frame_id[0..7]} instance=#{instance.id[0..7]}"
|
|
38
|
+
{ instantiated: true, instance_id: instance.id, frame_id: frame_id, filled_count: instance.filled_count }
|
|
39
|
+
else
|
|
40
|
+
{ instantiated: false, reason: :frame_not_found }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def add_frame_relation(frame_id:, relation:, target_frame_id:, **)
|
|
45
|
+
result = engine.add_frame_relation(frame_id: frame_id, relation: relation, target_frame_id: target_frame_id)
|
|
46
|
+
if result
|
|
47
|
+
Legion::Logging.debug "[frame_semantics] added relation #{relation} #{frame_id[0..7]}->#{target_frame_id[0..7]}"
|
|
48
|
+
{ added: true, frame_id: frame_id, relation: relation, target_frame_id: target_frame_id }
|
|
49
|
+
else
|
|
50
|
+
{ added: false, reason: :invalid_frame_or_relation }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def activate_semantic_frame(frame_id:, **)
|
|
55
|
+
result = engine.activate_frame(frame_id: frame_id)
|
|
56
|
+
if result
|
|
57
|
+
Legion::Logging.debug "[frame_semantics] activated frame #{frame_id[0..7]}"
|
|
58
|
+
{ activated: true, frame_id: frame_id }
|
|
59
|
+
else
|
|
60
|
+
{ activated: false, reason: :frame_not_found }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def active_frames_report(**)
|
|
65
|
+
frames = engine.active_frames
|
|
66
|
+
Legion::Logging.debug "[frame_semantics] active_frames count=#{frames.size}"
|
|
67
|
+
{ frames: frames.map(&:to_h), count: frames.size }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def related_frames_report(frame_id:, **)
|
|
71
|
+
frames = engine.related_frames(frame_id: frame_id)
|
|
72
|
+
Legion::Logging.debug "[frame_semantics] related_frames frame=#{frame_id[0..7]} count=#{frames.size}"
|
|
73
|
+
{ frames: frames.map(&:to_h), count: frames.size }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def complete_frames_report(**)
|
|
77
|
+
frames = engine.complete_frames
|
|
78
|
+
Legion::Logging.debug "[frame_semantics] complete_frames count=#{frames.size}"
|
|
79
|
+
{ frames: frames.map(&:to_h), count: frames.size }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def update_frame_semantics(**)
|
|
83
|
+
engine.decay_all
|
|
84
|
+
engine.prune_inactive
|
|
85
|
+
Legion::Logging.debug '[frame_semantics] decay_all + prune_inactive complete'
|
|
86
|
+
{ updated: true, stats: engine.to_h }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def frame_semantics_stats(**)
|
|
90
|
+
engine.to_h
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def engine
|
|
96
|
+
@engine ||= Helpers::FrameEngine.new
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/frame_semantics/version'
|
|
4
|
+
require 'legion/extensions/frame_semantics/helpers/constants'
|
|
5
|
+
require 'legion/extensions/frame_semantics/helpers/frame'
|
|
6
|
+
require 'legion/extensions/frame_semantics/helpers/frame_instance'
|
|
7
|
+
require 'legion/extensions/frame_semantics/helpers/frame_engine'
|
|
8
|
+
require 'legion/extensions/frame_semantics/runners/frame_semantics'
|
|
9
|
+
require 'legion/extensions/frame_semantics/helpers/client'
|
|
10
|
+
|
|
11
|
+
module Legion
|
|
12
|
+
module Extensions
|
|
13
|
+
module FrameSemantics
|
|
14
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::FrameSemantics::Helpers::FrameEngine do
|
|
4
|
+
subject(:engine) { described_class.new }
|
|
5
|
+
|
|
6
|
+
let(:frame) { engine.create_frame(name: :commercial_transaction, domain: :commerce) }
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
frame.add_slot(name: :buyer)
|
|
10
|
+
frame.add_slot(name: :seller)
|
|
11
|
+
frame.add_slot(name: :goods)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '#create_frame' do
|
|
15
|
+
it 'returns a Frame object' do
|
|
16
|
+
f = engine.create_frame(name: :motion, domain: :physics)
|
|
17
|
+
expect(f).to be_a(Legion::Extensions::FrameSemantics::Helpers::Frame)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'creates frame with slots from hash' do
|
|
21
|
+
f = engine.create_frame(
|
|
22
|
+
name: :competition,
|
|
23
|
+
domain: :sports,
|
|
24
|
+
slots: { winner: { type: :core, required: true }, loser: { type: :peripheral, required: false } }
|
|
25
|
+
)
|
|
26
|
+
expect(f.slots[:winner][:type]).to eq(:core)
|
|
27
|
+
expect(f.slots[:loser][:type]).to eq(:peripheral)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe '#define_slot' do
|
|
32
|
+
it 'adds a slot to an existing frame' do
|
|
33
|
+
result = engine.define_slot(frame_id: frame.id, name: :price)
|
|
34
|
+
expect(result).to be_a(Legion::Extensions::FrameSemantics::Helpers::Frame)
|
|
35
|
+
expect(frame.slots[:price]).not_to be_nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'returns nil for unknown frame_id' do
|
|
39
|
+
expect(engine.define_slot(frame_id: 'bogus', name: :price)).to be_nil
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe '#fill_slot' do
|
|
44
|
+
it 'fills a slot and returns true' do
|
|
45
|
+
expect(engine.fill_slot(frame_id: frame.id, slot_name: :buyer, filler: 'Alice')).to be true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'returns false for unknown frame' do
|
|
49
|
+
expect(engine.fill_slot(frame_id: 'bogus', slot_name: :buyer, filler: 'x')).to be false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'activates the frame when slot is filled' do
|
|
53
|
+
initial = frame.activation
|
|
54
|
+
engine.fill_slot(frame_id: frame.id, slot_name: :buyer, filler: 'Alice')
|
|
55
|
+
expect(frame.activation_count).to be >= 1
|
|
56
|
+
expect(frame.activation).to be >= initial
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe '#instantiate_frame' do
|
|
61
|
+
before do
|
|
62
|
+
engine.fill_slot(frame_id: frame.id, slot_name: :buyer, filler: 'Alice')
|
|
63
|
+
engine.fill_slot(frame_id: frame.id, slot_name: :seller, filler: 'Bob')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'creates a FrameInstance snapshot' do
|
|
67
|
+
inst = engine.instantiate_frame(frame_id: frame.id, context: 'coffee shop')
|
|
68
|
+
expect(inst).to be_a(Legion::Extensions::FrameSemantics::Helpers::FrameInstance)
|
|
69
|
+
expect(inst.frame_id).to eq(frame.id)
|
|
70
|
+
expect(inst.context).to eq('coffee shop')
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'returns nil for unknown frame' do
|
|
74
|
+
expect(engine.instantiate_frame(frame_id: 'bogus', context: 'x')).to be_nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'snapshots current slot fillers' do
|
|
78
|
+
inst = engine.instantiate_frame(frame_id: frame.id, context: 'test')
|
|
79
|
+
expect(inst.slot_fillers[:buyer]).to eq('Alice')
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
describe '#add_frame_relation' do
|
|
84
|
+
it 'adds a valid relation' do
|
|
85
|
+
target = engine.create_frame(name: :transfer, domain: :commerce)
|
|
86
|
+
result = engine.add_frame_relation(
|
|
87
|
+
frame_id: frame.id,
|
|
88
|
+
relation: :has_subframe,
|
|
89
|
+
target_frame_id: target.id
|
|
90
|
+
)
|
|
91
|
+
expect(result).to be true
|
|
92
|
+
expect(frame.relations.size).to eq(1)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'rejects invalid relation type' do
|
|
96
|
+
result = engine.add_frame_relation(
|
|
97
|
+
frame_id: frame.id,
|
|
98
|
+
relation: :invalid_relation,
|
|
99
|
+
target_frame_id: SecureRandom.uuid
|
|
100
|
+
)
|
|
101
|
+
expect(result).to be false
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'returns false for unknown frame_id' do
|
|
105
|
+
expect(
|
|
106
|
+
engine.add_frame_relation(frame_id: 'bogus', relation: :uses, target_frame_id: SecureRandom.uuid)
|
|
107
|
+
).to be false
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
describe '#activate_frame' do
|
|
112
|
+
it 'activates an existing frame' do
|
|
113
|
+
initial_count = frame.activation_count
|
|
114
|
+
engine.activate_frame(frame_id: frame.id)
|
|
115
|
+
expect(frame.activation_count).to eq(initial_count + 1)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'returns false for unknown frame' do
|
|
119
|
+
expect(engine.activate_frame(frame_id: 'bogus')).to be false
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
describe '#active_frames' do
|
|
124
|
+
it 'returns frames with activation > 0.5' do
|
|
125
|
+
5.times { engine.activate_frame(frame_id: frame.id) }
|
|
126
|
+
expect(engine.active_frames).to include(frame)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'excludes frames below threshold' do
|
|
130
|
+
new_engine = described_class.new
|
|
131
|
+
f = new_engine.create_frame(name: :cold, domain: :test)
|
|
132
|
+
100.times { f.decay! }
|
|
133
|
+
expect(new_engine.active_frames).not_to include(f)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
describe '#frames_by_domain' do
|
|
138
|
+
before do
|
|
139
|
+
engine.create_frame(name: :motion, domain: :physics)
|
|
140
|
+
engine.create_frame(name: :competition, domain: :sports)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'returns only frames matching the domain' do
|
|
144
|
+
result = engine.frames_by_domain(domain: :physics)
|
|
145
|
+
expect(result.map(&:domain)).to all(eq(:physics))
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
describe '#related_frames' do
|
|
150
|
+
it 'traverses relations to return related frames' do
|
|
151
|
+
target = engine.create_frame(name: :payment, domain: :commerce)
|
|
152
|
+
engine.add_frame_relation(frame_id: frame.id, relation: :has_subframe, target_frame_id: target.id)
|
|
153
|
+
related = engine.related_frames(frame_id: frame.id)
|
|
154
|
+
expect(related).to include(target)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it 'returns empty array for unknown frame' do
|
|
158
|
+
expect(engine.related_frames(frame_id: 'bogus')).to eq([])
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
describe '#most_activated' do
|
|
163
|
+
it 'returns top N frames sorted by activation desc' do
|
|
164
|
+
f2 = engine.create_frame(name: :motion, domain: :physics)
|
|
165
|
+
5.times { engine.activate_frame(frame_id: frame.id) }
|
|
166
|
+
result = engine.most_activated(limit: 2)
|
|
167
|
+
expect(result.first).to eq(frame)
|
|
168
|
+
expect(result).not_to include(f2) if result.size < 2
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
describe '#instances_for_frame' do
|
|
173
|
+
it 'returns all instances of a specific frame' do
|
|
174
|
+
engine.instantiate_frame(frame_id: frame.id, context: 'ctx1')
|
|
175
|
+
engine.instantiate_frame(frame_id: frame.id, context: 'ctx2')
|
|
176
|
+
expect(engine.instances_for_frame(frame_id: frame.id).size).to eq(2)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
describe '#complete_frames' do
|
|
181
|
+
it 'returns only complete frames' do
|
|
182
|
+
engine.fill_slot(frame_id: frame.id, slot_name: :buyer, filler: 'Alice')
|
|
183
|
+
engine.fill_slot(frame_id: frame.id, slot_name: :seller, filler: 'Bob')
|
|
184
|
+
engine.fill_slot(frame_id: frame.id, slot_name: :goods, filler: 'coffee')
|
|
185
|
+
expect(engine.complete_frames).to include(frame)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'excludes incomplete frames' do
|
|
189
|
+
engine.fill_slot(frame_id: frame.id, slot_name: :buyer, filler: 'Alice')
|
|
190
|
+
expect(engine.complete_frames).not_to include(frame)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
describe '#decay_all' do
|
|
195
|
+
it 'decays all frame activations' do
|
|
196
|
+
initial = frame.activation
|
|
197
|
+
engine.decay_all
|
|
198
|
+
expect(frame.activation).to be < initial
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
describe '#prune_inactive' do
|
|
203
|
+
it 'removes frames with activation <= 0.05' do
|
|
204
|
+
f = engine.create_frame(name: :ghost, domain: :test)
|
|
205
|
+
100.times { f.decay! }
|
|
206
|
+
engine.prune_inactive
|
|
207
|
+
expect(engine.instances_for_frame(frame_id: f.id)).to be_empty
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it 'retains frames with activation above threshold' do
|
|
211
|
+
5.times { engine.activate_frame(frame_id: frame.id) }
|
|
212
|
+
engine.prune_inactive
|
|
213
|
+
expect(engine.active_frames).to include(frame)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
describe '#to_h' do
|
|
218
|
+
it 'returns stats hash with expected keys' do
|
|
219
|
+
h = engine.to_h
|
|
220
|
+
expect(h).to include(:frame_count, :instance_count, :active_count, :complete_count, :domains)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'reflects current state' do
|
|
224
|
+
expect(engine.to_h[:frame_count]).to eq(1)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::FrameSemantics::Helpers::FrameInstance do
|
|
4
|
+
let(:frame_id) { SecureRandom.uuid }
|
|
5
|
+
let(:frame_name) { :commercial_transaction }
|
|
6
|
+
let(:slot_fillers) do
|
|
7
|
+
{ buyer: 'Alice', seller: 'Bob', goods: nil }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
subject(:instance) do
|
|
11
|
+
described_class.new(
|
|
12
|
+
frame_id: frame_id,
|
|
13
|
+
frame_name: frame_name,
|
|
14
|
+
slot_fillers: slot_fillers,
|
|
15
|
+
context: 'buying coffee',
|
|
16
|
+
confidence: 0.85
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe '#initialize' do
|
|
21
|
+
it 'assigns a uuid id' do
|
|
22
|
+
expect(instance.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'stores frame metadata' do
|
|
26
|
+
expect(instance.frame_id).to eq(frame_id)
|
|
27
|
+
expect(instance.frame_name).to eq(frame_name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'duplicates slot_fillers' do
|
|
31
|
+
inst = instance
|
|
32
|
+
slot_fillers[:buyer] = 'Charlie'
|
|
33
|
+
expect(inst.slot_fillers[:buyer]).to eq('Alice')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'stores context and confidence' do
|
|
37
|
+
expect(instance.context).to eq('buying coffee')
|
|
38
|
+
expect(instance.confidence).to eq(0.85)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '#filled_count' do
|
|
43
|
+
it 'counts non-nil fillers' do
|
|
44
|
+
expect(instance.filled_count).to eq(2)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'returns 0 when all fillers are nil' do
|
|
48
|
+
empty = described_class.new(
|
|
49
|
+
frame_id: frame_id, frame_name: frame_name,
|
|
50
|
+
slot_fillers: { buyer: nil }, context: 'x'
|
|
51
|
+
)
|
|
52
|
+
expect(empty.filled_count).to eq(0)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe '#complete?' do
|
|
57
|
+
it 'returns true when at least one slot is filled' do
|
|
58
|
+
expect(instance.complete?).to be true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'returns false when all slots are nil' do
|
|
62
|
+
empty = described_class.new(
|
|
63
|
+
frame_id: frame_id, frame_name: frame_name,
|
|
64
|
+
slot_fillers: { buyer: nil }, context: 'x'
|
|
65
|
+
)
|
|
66
|
+
expect(empty.complete?).to be false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe '#to_h' do
|
|
71
|
+
it 'returns expected keys' do
|
|
72
|
+
h = instance.to_h
|
|
73
|
+
expect(h).to include(:id, :frame_id, :frame_name, :slot_fillers,
|
|
74
|
+
:context, :confidence, :filled_count, :complete, :created_at)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'includes filled_count and complete flag' do
|
|
78
|
+
h = instance.to_h
|
|
79
|
+
expect(h[:filled_count]).to eq(2)
|
|
80
|
+
expect(h[:complete]).to be true
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::FrameSemantics::Helpers::Frame do
|
|
4
|
+
subject(:frame) { described_class.new(name: :commercial_transaction, domain: :commerce) }
|
|
5
|
+
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'assigns a uuid id' do
|
|
8
|
+
expect(frame.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'sets name and domain' do
|
|
12
|
+
expect(frame.name).to eq(:commercial_transaction)
|
|
13
|
+
expect(frame.domain).to eq(:commerce)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'starts with default activation' do
|
|
17
|
+
expect(frame.activation).to eq(Legion::Extensions::FrameSemantics::Helpers::DEFAULT_ACTIVATION)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'starts with zero activation_count' do
|
|
21
|
+
expect(frame.activation_count).to eq(0)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'starts with empty slots and relations' do
|
|
25
|
+
expect(frame.slots).to be_empty
|
|
26
|
+
expect(frame.relations).to be_empty
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe '#add_slot' do
|
|
31
|
+
it 'adds a core slot by default' do
|
|
32
|
+
frame.add_slot(name: :buyer)
|
|
33
|
+
expect(frame.slots[:buyer][:type]).to eq(:core)
|
|
34
|
+
expect(frame.slots[:buyer][:required]).to be true
|
|
35
|
+
expect(frame.slots[:buyer][:filler]).to be_nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'adds a peripheral slot when specified' do
|
|
39
|
+
frame.add_slot(name: :payment_method, slot_type: :peripheral, required: false)
|
|
40
|
+
expect(frame.slots[:payment_method][:type]).to eq(:peripheral)
|
|
41
|
+
expect(frame.slots[:payment_method][:required]).to be false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'returns self for chaining' do
|
|
45
|
+
result = frame.add_slot(name: :seller)
|
|
46
|
+
expect(result).to be(frame)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#fill_slot' do
|
|
51
|
+
before { frame.add_slot(name: :buyer) }
|
|
52
|
+
|
|
53
|
+
it 'fills a slot and returns true' do
|
|
54
|
+
result = frame.fill_slot(name: :buyer, filler: 'Alice')
|
|
55
|
+
expect(result).to be true
|
|
56
|
+
expect(frame.slots[:buyer][:filler]).to eq('Alice')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'boosts activation when slot is filled' do
|
|
60
|
+
initial = frame.activation
|
|
61
|
+
frame.fill_slot(name: :buyer, filler: 'Alice')
|
|
62
|
+
expect(frame.activation).to be > initial
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'returns false for unknown slot' do
|
|
66
|
+
expect(frame.fill_slot(name: :unknown, filler: 'x')).to be false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe '#clear_slot' do
|
|
71
|
+
before do
|
|
72
|
+
frame.add_slot(name: :buyer)
|
|
73
|
+
frame.fill_slot(name: :buyer, filler: 'Alice')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'clears the slot filler' do
|
|
77
|
+
frame.clear_slot(name: :buyer)
|
|
78
|
+
expect(frame.slots[:buyer][:filler]).to be_nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'returns true for existing slot' do
|
|
82
|
+
expect(frame.clear_slot(name: :buyer)).to be true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'returns false for unknown slot' do
|
|
86
|
+
expect(frame.clear_slot(name: :ghost)).to be false
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe '#core_slots' do
|
|
91
|
+
before do
|
|
92
|
+
frame.add_slot(name: :buyer, slot_type: :core)
|
|
93
|
+
frame.add_slot(name: :seller, slot_type: :core)
|
|
94
|
+
frame.add_slot(name: :note, slot_type: :peripheral)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'returns only core slots' do
|
|
98
|
+
expect(frame.core_slots.keys).to contain_exactly(:buyer, :seller)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
describe '#filled_slots' do
|
|
103
|
+
before do
|
|
104
|
+
frame.add_slot(name: :buyer)
|
|
105
|
+
frame.add_slot(name: :seller)
|
|
106
|
+
frame.fill_slot(name: :buyer, filler: 'Alice')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'returns only filled slots' do
|
|
110
|
+
expect(frame.filled_slots.keys).to eq([:buyer])
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
describe '#completion_ratio' do
|
|
115
|
+
it 'returns 0.0 with no slots' do
|
|
116
|
+
expect(frame.completion_ratio).to eq(0.0)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'returns correct fraction of filled core slots' do
|
|
120
|
+
frame.add_slot(name: :buyer)
|
|
121
|
+
frame.add_slot(name: :seller)
|
|
122
|
+
frame.add_slot(name: :goods)
|
|
123
|
+
frame.fill_slot(name: :buyer, filler: 'Alice')
|
|
124
|
+
expect(frame.completion_ratio).to be_within(0.001).of(1.0 / 3.0)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'ignores peripheral slots in ratio' do
|
|
128
|
+
frame.add_slot(name: :buyer, slot_type: :core)
|
|
129
|
+
frame.add_slot(name: :note, slot_type: :peripheral)
|
|
130
|
+
frame.fill_slot(name: :buyer, filler: 'Alice')
|
|
131
|
+
expect(frame.completion_ratio).to eq(1.0)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
describe '#complete?' do
|
|
136
|
+
it 'returns false when completion is below threshold' do
|
|
137
|
+
frame.add_slot(name: :buyer)
|
|
138
|
+
frame.add_slot(name: :seller)
|
|
139
|
+
frame.add_slot(name: :goods)
|
|
140
|
+
frame.fill_slot(name: :buyer, filler: 'Alice')
|
|
141
|
+
expect(frame.complete?).to be false
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it 'returns true when all core slots are filled' do
|
|
145
|
+
frame.add_slot(name: :buyer)
|
|
146
|
+
frame.fill_slot(name: :buyer, filler: 'Alice')
|
|
147
|
+
expect(frame.complete?).to be true
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
describe '#activate!' do
|
|
152
|
+
it 'boosts activation and increments count' do
|
|
153
|
+
initial = frame.activation
|
|
154
|
+
frame.activate!
|
|
155
|
+
expect(frame.activation).to be > initial
|
|
156
|
+
expect(frame.activation_count).to eq(1)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'clamps activation at 1.0' do
|
|
160
|
+
10.times { frame.activate! }
|
|
161
|
+
expect(frame.activation).to eq(1.0)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
describe '#decay!' do
|
|
166
|
+
it 'reduces activation' do
|
|
167
|
+
initial = frame.activation
|
|
168
|
+
frame.decay!
|
|
169
|
+
expect(frame.activation).to be < initial
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it 'clamps activation at 0.0' do
|
|
173
|
+
100.times { frame.decay! }
|
|
174
|
+
expect(frame.activation).to eq(0.0)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
describe '#activation_label' do
|
|
179
|
+
it 'returns :dominant for high activation' do
|
|
180
|
+
10.times { frame.activate! }
|
|
181
|
+
expect(frame.activation_label).to eq(:dominant)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'returns :inactive for very low activation' do
|
|
185
|
+
100.times { frame.decay! }
|
|
186
|
+
expect(frame.activation_label).to eq(:inactive)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it 'returns :primed for mid activation around 0.5' do
|
|
190
|
+
frame.instance_variable_set(:@activation, 0.5)
|
|
191
|
+
expect(frame.activation_label).to eq(:primed)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
describe '#add_relation' do
|
|
196
|
+
it 'appends a relation entry' do
|
|
197
|
+
other_id = SecureRandom.uuid
|
|
198
|
+
frame.add_relation(relation: :inherits_from, target_frame_id: other_id)
|
|
199
|
+
expect(frame.relations.size).to eq(1)
|
|
200
|
+
expect(frame.relations.first[:relation]).to eq(:inherits_from)
|
|
201
|
+
expect(frame.relations.first[:target_frame_id]).to eq(other_id)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
describe '#to_h' do
|
|
206
|
+
it 'returns a hash with expected keys' do
|
|
207
|
+
h = frame.to_h
|
|
208
|
+
expect(h).to include(:id, :name, :domain, :slots, :relations,
|
|
209
|
+
:activation, :activation_label, :activation_count,
|
|
210
|
+
:completion_ratio, :complete, :created_at)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/frame_semantics/helpers/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::FrameSemantics::Runners::FrameSemantics do
|
|
6
|
+
let(:client) { Legion::Extensions::FrameSemantics::Helpers::Client.new }
|
|
7
|
+
|
|
8
|
+
describe '#create_semantic_frame' do
|
|
9
|
+
it 'creates a frame and returns its id' do
|
|
10
|
+
result = client.create_semantic_frame(name: :commercial_transaction, domain: :commerce)
|
|
11
|
+
expect(result[:created]).to be true
|
|
12
|
+
expect(result[:frame_id]).to match(/\A[0-9a-f-]{36}\z/)
|
|
13
|
+
expect(result[:name]).to eq(:commercial_transaction)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe '#define_frame_slot' do
|
|
18
|
+
let(:frame_id) { client.create_semantic_frame(name: :motion, domain: :physics)[:frame_id] }
|
|
19
|
+
|
|
20
|
+
it 'adds a slot to a known frame' do
|
|
21
|
+
result = client.define_frame_slot(frame_id: frame_id, name: :mover)
|
|
22
|
+
expect(result[:defined]).to be true
|
|
23
|
+
expect(result[:slot_name]).to eq(:mover)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'returns not found for unknown frame' do
|
|
27
|
+
result = client.define_frame_slot(frame_id: 'bogus', name: :mover)
|
|
28
|
+
expect(result[:defined]).to be false
|
|
29
|
+
expect(result[:reason]).to eq(:frame_not_found)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe '#fill_frame_slot' do
|
|
34
|
+
let(:frame_id) do
|
|
35
|
+
id = client.create_semantic_frame(name: :motion, domain: :physics)[:frame_id]
|
|
36
|
+
client.define_frame_slot(frame_id: id, name: :mover)
|
|
37
|
+
id
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'fills an existing slot' do
|
|
41
|
+
result = client.fill_frame_slot(frame_id: frame_id, slot_name: :mover, filler: 'the car')
|
|
42
|
+
expect(result[:filled]).to be true
|
|
43
|
+
expect(result[:filler]).to eq('the car')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'returns error for unknown slot' do
|
|
47
|
+
result = client.fill_frame_slot(frame_id: frame_id, slot_name: :nonexistent, filler: 'x')
|
|
48
|
+
expect(result[:filled]).to be false
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe '#instantiate_semantic_frame' do
|
|
53
|
+
let(:frame_id) do
|
|
54
|
+
id = client.create_semantic_frame(name: :motion, domain: :physics)[:frame_id]
|
|
55
|
+
client.define_frame_slot(frame_id: id, name: :mover)
|
|
56
|
+
client.fill_frame_slot(frame_id: id, slot_name: :mover, filler: 'rocket')
|
|
57
|
+
id
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'creates an instance snapshot' do
|
|
61
|
+
result = client.instantiate_semantic_frame(frame_id: frame_id, context: 'test context')
|
|
62
|
+
expect(result[:instantiated]).to be true
|
|
63
|
+
expect(result[:instance_id]).to match(/\A[0-9a-f-]{36}\z/)
|
|
64
|
+
expect(result[:filled_count]).to eq(1)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'returns error for unknown frame' do
|
|
68
|
+
result = client.instantiate_semantic_frame(frame_id: 'bogus', context: 'x')
|
|
69
|
+
expect(result[:instantiated]).to be false
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe '#add_frame_relation' do
|
|
74
|
+
let(:frame_id) { client.create_semantic_frame(name: :transfer, domain: :commerce)[:frame_id] }
|
|
75
|
+
let(:target_id) { client.create_semantic_frame(name: :payment, domain: :commerce)[:frame_id] }
|
|
76
|
+
|
|
77
|
+
it 'adds a valid frame relation' do
|
|
78
|
+
result = client.add_frame_relation(
|
|
79
|
+
frame_id: frame_id, relation: :has_subframe, target_frame_id: target_id
|
|
80
|
+
)
|
|
81
|
+
expect(result[:added]).to be true
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'rejects an invalid relation' do
|
|
85
|
+
result = client.add_frame_relation(
|
|
86
|
+
frame_id: frame_id, relation: :nonsense, target_frame_id: target_id
|
|
87
|
+
)
|
|
88
|
+
expect(result[:added]).to be false
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe '#activate_semantic_frame' do
|
|
93
|
+
let(:frame_id) { client.create_semantic_frame(name: :competition, domain: :sports)[:frame_id] }
|
|
94
|
+
|
|
95
|
+
it 'activates the frame' do
|
|
96
|
+
result = client.activate_semantic_frame(frame_id: frame_id)
|
|
97
|
+
expect(result[:activated]).to be true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'returns error for unknown frame' do
|
|
101
|
+
result = client.activate_semantic_frame(frame_id: 'bogus')
|
|
102
|
+
expect(result[:activated]).to be false
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
describe '#active_frames_report' do
|
|
107
|
+
it 'returns list of active frames' do
|
|
108
|
+
id = client.create_semantic_frame(name: :competition, domain: :sports)[:frame_id]
|
|
109
|
+
4.times { client.activate_semantic_frame(frame_id: id) }
|
|
110
|
+
result = client.active_frames_report
|
|
111
|
+
expect(result).to have_key(:frames)
|
|
112
|
+
expect(result).to have_key(:count)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
describe '#related_frames_report' do
|
|
117
|
+
let(:frame_id) { client.create_semantic_frame(name: :buying, domain: :commerce)[:frame_id] }
|
|
118
|
+
let(:target_id) { client.create_semantic_frame(name: :paying, domain: :commerce)[:frame_id] }
|
|
119
|
+
|
|
120
|
+
before { client.add_frame_relation(frame_id: frame_id, relation: :has_subframe, target_frame_id: target_id) }
|
|
121
|
+
|
|
122
|
+
it 'returns related frames' do
|
|
123
|
+
result = client.related_frames_report(frame_id: frame_id)
|
|
124
|
+
expect(result[:count]).to eq(1)
|
|
125
|
+
expect(result[:frames].first[:id]).to eq(target_id)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe '#complete_frames_report' do
|
|
130
|
+
it 'returns complete frames' do
|
|
131
|
+
id = client.create_semantic_frame(name: :simple, domain: :test)[:frame_id]
|
|
132
|
+
client.define_frame_slot(frame_id: id, name: :actor)
|
|
133
|
+
client.fill_frame_slot(frame_id: id, slot_name: :actor, filler: 'someone')
|
|
134
|
+
result = client.complete_frames_report
|
|
135
|
+
expect(result[:count]).to be >= 1
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
describe '#update_frame_semantics' do
|
|
140
|
+
it 'runs decay and prune, returning updated status' do
|
|
141
|
+
result = client.update_frame_semantics
|
|
142
|
+
expect(result[:updated]).to be true
|
|
143
|
+
expect(result[:stats]).to have_key(:frame_count)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
describe '#frame_semantics_stats' do
|
|
148
|
+
it 'returns engine stats' do
|
|
149
|
+
client.create_semantic_frame(name: :motion, domain: :physics)
|
|
150
|
+
result = client.frame_semantics_stats
|
|
151
|
+
expect(result[:frame_count]).to be >= 1
|
|
152
|
+
expect(result).to have_key(:domains)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -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/frame_semantics'
|
|
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,76 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-frame-semantics
|
|
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: Fillmore's Frame Semantics engine for LegionIO — conceptual frame activation,
|
|
27
|
+
slot filling, and instance creation
|
|
28
|
+
email:
|
|
29
|
+
- matthewdiverson@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- Gemfile
|
|
35
|
+
- lex-frame-semantics.gemspec
|
|
36
|
+
- lib/legion/extensions/frame_semantics.rb
|
|
37
|
+
- lib/legion/extensions/frame_semantics/helpers/client.rb
|
|
38
|
+
- lib/legion/extensions/frame_semantics/helpers/constants.rb
|
|
39
|
+
- lib/legion/extensions/frame_semantics/helpers/frame.rb
|
|
40
|
+
- lib/legion/extensions/frame_semantics/helpers/frame_engine.rb
|
|
41
|
+
- lib/legion/extensions/frame_semantics/helpers/frame_instance.rb
|
|
42
|
+
- lib/legion/extensions/frame_semantics/runners/frame_semantics.rb
|
|
43
|
+
- lib/legion/extensions/frame_semantics/version.rb
|
|
44
|
+
- spec/legion/extensions/frame_semantics/helpers/frame_engine_spec.rb
|
|
45
|
+
- spec/legion/extensions/frame_semantics/helpers/frame_instance_spec.rb
|
|
46
|
+
- spec/legion/extensions/frame_semantics/helpers/frame_spec.rb
|
|
47
|
+
- spec/legion/extensions/frame_semantics/runners/frame_semantics_spec.rb
|
|
48
|
+
- spec/spec_helper.rb
|
|
49
|
+
homepage: https://github.com/LegionIO/lex-frame-semantics
|
|
50
|
+
licenses:
|
|
51
|
+
- MIT
|
|
52
|
+
metadata:
|
|
53
|
+
homepage_uri: https://github.com/LegionIO/lex-frame-semantics
|
|
54
|
+
source_code_uri: https://github.com/LegionIO/lex-frame-semantics
|
|
55
|
+
documentation_uri: https://github.com/LegionIO/lex-frame-semantics
|
|
56
|
+
changelog_uri: https://github.com/LegionIO/lex-frame-semantics
|
|
57
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-frame-semantics/issues
|
|
58
|
+
rubygems_mfa_required: 'true'
|
|
59
|
+
rdoc_options: []
|
|
60
|
+
require_paths:
|
|
61
|
+
- lib
|
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '3.4'
|
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '0'
|
|
72
|
+
requirements: []
|
|
73
|
+
rubygems_version: 3.6.9
|
|
74
|
+
specification_version: 4
|
|
75
|
+
summary: LEX Frame Semantics
|
|
76
|
+
test_files: []
|