lex-surprise 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/lib/legion/extensions/surprise/client.rb +18 -0
- data/lib/legion/extensions/surprise/helpers/constants.rb +31 -0
- data/lib/legion/extensions/surprise/helpers/habituation_model.rb +58 -0
- data/lib/legion/extensions/surprise/helpers/surprise_event.rb +39 -0
- data/lib/legion/extensions/surprise/helpers/surprise_store.rb +70 -0
- data/lib/legion/extensions/surprise/runners/surprise.rb +170 -0
- data/lib/legion/extensions/surprise/version.rb +9 -0
- data/lib/legion/extensions/surprise.rb +17 -0
- metadata +65 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 22083a1c0263ca15e137e1df847e5c4f7619a6148116adb37a3f587c9ad4a379
|
|
4
|
+
data.tar.gz: b0d6ae0f77e5574d2ea4d400c1df123e5a64f1af9e12e35b88f532b6b0658d0b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9477cfc33f2e546dd75d6ab8bceb71f52a1c468d4555a0f914922185909a03de7fb16fe95c46ef06413aeaaac9191b67f661dd74a6b26af5e7909814380b44a1
|
|
7
|
+
data.tar.gz: 981d88fe05c424d1a2ee6fa21afd09fbf7b1a770fc93fb5f2175c33f9ca4f32ba5ccfceff918a6eef69b87bbc048ee07afd82e3869e288dfa6948057769b8c3a
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Surprise
|
|
6
|
+
class Client
|
|
7
|
+
include Runners::Surprise
|
|
8
|
+
|
|
9
|
+
attr_reader :store, :habituation_model
|
|
10
|
+
|
|
11
|
+
def initialize(store: nil, habituation_model: nil, **)
|
|
12
|
+
@store = store || Helpers::SurpriseStore.new
|
|
13
|
+
@habituation_model = habituation_model || Helpers::HabituationModel.new
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Surprise
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
SURPRISE_THRESHOLD = 0.4
|
|
9
|
+
|
|
10
|
+
HABITUATION_RATE = 0.05
|
|
11
|
+
|
|
12
|
+
SENSITIZATION_RATE = 0.02
|
|
13
|
+
|
|
14
|
+
SURPRISE_DECAY = 0.1
|
|
15
|
+
|
|
16
|
+
MAX_SURPRISE_HISTORY = 200
|
|
17
|
+
|
|
18
|
+
SURPRISE_ALPHA = 0.15
|
|
19
|
+
|
|
20
|
+
VALENCE_WEIGHTS = { positive: 0.6, negative: 1.0, neutral: 0.3 }.freeze
|
|
21
|
+
|
|
22
|
+
DOMAIN_HABITUATION_FLOOR = 0.1
|
|
23
|
+
|
|
24
|
+
MAX_DOMAINS = 50
|
|
25
|
+
|
|
26
|
+
ORIENTING_COOLDOWN = 3
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Surprise
|
|
6
|
+
module Helpers
|
|
7
|
+
class HabituationModel
|
|
8
|
+
def initialize
|
|
9
|
+
@levels = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def sensitivity_for(domain)
|
|
13
|
+
@levels.fetch(domain, 1.0)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def habituate(domain)
|
|
17
|
+
current = sensitivity_for(domain)
|
|
18
|
+
floor = Constants::DOMAIN_HABITUATION_FLOOR
|
|
19
|
+
updated = [current - Constants::HABITUATION_RATE, floor].max
|
|
20
|
+
@levels[domain] = updated
|
|
21
|
+
enforce_domain_limit
|
|
22
|
+
updated
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def sensitize(domain)
|
|
26
|
+
current = sensitivity_for(domain)
|
|
27
|
+
updated = [current + Constants::SENSITIZATION_RATE, 1.0].min
|
|
28
|
+
@levels[domain] = updated
|
|
29
|
+
updated
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def decay_all
|
|
33
|
+
@levels.each_key do |domain|
|
|
34
|
+
current = @levels[domain]
|
|
35
|
+
@levels[domain] = [current + (Constants::SENSITIZATION_RATE * 0.5), 1.0].min
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_h
|
|
40
|
+
{
|
|
41
|
+
domains: @levels.size,
|
|
42
|
+
sensitivities: @levels.transform_values { |v| v.round(4) }
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def enforce_domain_limit
|
|
49
|
+
return unless @levels.size > Constants::MAX_DOMAINS
|
|
50
|
+
|
|
51
|
+
oldest_key = @levels.keys.first
|
|
52
|
+
@levels.delete(oldest_key)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Surprise
|
|
8
|
+
module Helpers
|
|
9
|
+
class SurpriseEvent
|
|
10
|
+
attr_reader :id, :domain, :predicted, :actual, :magnitude, :valence, :timestamp, :orienting
|
|
11
|
+
|
|
12
|
+
def initialize(domain:, predicted:, actual:, magnitude:, valence:, orienting: false) # rubocop:disable Metrics/ParameterLists
|
|
13
|
+
@id = SecureRandom.uuid
|
|
14
|
+
@domain = domain
|
|
15
|
+
@predicted = predicted
|
|
16
|
+
@actual = actual
|
|
17
|
+
@magnitude = magnitude.clamp(0.0, 1.0)
|
|
18
|
+
@valence = valence
|
|
19
|
+
@orienting = orienting
|
|
20
|
+
@timestamp = Time.now.utc
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_h
|
|
24
|
+
{
|
|
25
|
+
id: @id,
|
|
26
|
+
domain: @domain,
|
|
27
|
+
predicted: @predicted,
|
|
28
|
+
actual: @actual,
|
|
29
|
+
magnitude: @magnitude.round(4),
|
|
30
|
+
valence: @valence,
|
|
31
|
+
orienting: @orienting,
|
|
32
|
+
timestamp: @timestamp
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Surprise
|
|
6
|
+
module Helpers
|
|
7
|
+
class SurpriseStore
|
|
8
|
+
attr_reader :events
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@events = []
|
|
12
|
+
@baselines = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def record(event)
|
|
16
|
+
@events << event
|
|
17
|
+
update_baseline(event.domain, event.magnitude)
|
|
18
|
+
trim
|
|
19
|
+
event
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def recent(count = 10)
|
|
23
|
+
@events.last(count)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def by_domain(domain)
|
|
27
|
+
@events.select { |e| e.domain == domain }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def most_surprising(count = 5)
|
|
31
|
+
@events.sort_by { |e| -e.magnitude }.first(count)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def baseline_for(domain)
|
|
35
|
+
@baselines.fetch(domain, 0.0)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_h
|
|
39
|
+
total = @events.size
|
|
40
|
+
domains = @events.map(&:domain).uniq
|
|
41
|
+
avg_mag = total.positive? ? (@events.sum(&:magnitude) / total).round(4) : 0.0
|
|
42
|
+
top = most_surprising(1).first
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
total_events: total,
|
|
46
|
+
domain_count: domains.size,
|
|
47
|
+
average_magnitude: avg_mag,
|
|
48
|
+
most_surprising_domain: top&.domain,
|
|
49
|
+
baselines: @baselines.transform_values { |v| v.round(4) }
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def update_baseline(domain, magnitude)
|
|
56
|
+
prior = @baselines.fetch(domain, magnitude)
|
|
57
|
+
alpha = Constants::SURPRISE_ALPHA
|
|
58
|
+
@baselines[domain] = ((1 - alpha) * prior) + (alpha * magnitude)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def trim
|
|
62
|
+
return unless @events.size > Constants::MAX_SURPRISE_HISTORY
|
|
63
|
+
|
|
64
|
+
@events.shift(@events.size - Constants::MAX_SURPRISE_HISTORY)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Surprise
|
|
6
|
+
module Runners
|
|
7
|
+
module Surprise
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
# Evaluate a single prediction-outcome pair and compute surprise magnitude.
|
|
12
|
+
# magnitude = |predicted - actual| * sensitivity * valence_weight, clamped to [0,1]
|
|
13
|
+
def evaluate_surprise(domain:, predicted:, actual:, valence: :neutral, **)
|
|
14
|
+
sensitivity = habituation_model.sensitivity_for(domain)
|
|
15
|
+
valence_weight = Helpers::Constants::VALENCE_WEIGHTS.fetch(valence, 0.3)
|
|
16
|
+
raw_diff = (predicted.to_f - actual.to_f).abs
|
|
17
|
+
magnitude = (raw_diff * sensitivity * valence_weight).clamp(0.0, 1.0)
|
|
18
|
+
|
|
19
|
+
threshold = Helpers::Constants::SURPRISE_THRESHOLD
|
|
20
|
+
orienting = should_orient?(domain, magnitude, threshold)
|
|
21
|
+
|
|
22
|
+
event = Helpers::SurpriseEvent.new(
|
|
23
|
+
domain: domain,
|
|
24
|
+
predicted: predicted,
|
|
25
|
+
actual: actual,
|
|
26
|
+
magnitude: magnitude,
|
|
27
|
+
valence: valence,
|
|
28
|
+
orienting: orienting
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
store.record(event)
|
|
32
|
+
habituation_model.habituate(domain)
|
|
33
|
+
|
|
34
|
+
if orienting
|
|
35
|
+
record_cooldown(domain)
|
|
36
|
+
Legion::Logging.debug "[surprise] orienting response triggered: domain=#{domain} magnitude=#{magnitude.round(3)}"
|
|
37
|
+
else
|
|
38
|
+
Legion::Logging.debug "[surprise] surprise recorded: domain=#{domain} magnitude=#{magnitude.round(3)} orienting=false"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
{ success: true, surprise_event: event.to_h, orienting_triggered: orienting }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Per-tick update: extract domain predictions from tick_result, compute surprise for each,
|
|
45
|
+
# decay the store's baseline tracking, and return a summary.
|
|
46
|
+
def update_surprise(tick_result: {}, **)
|
|
47
|
+
predictions = extract_predictions(tick_result)
|
|
48
|
+
events = []
|
|
49
|
+
|
|
50
|
+
predictions.each do |pred|
|
|
51
|
+
next unless pred[:domain] && !pred[:predicted].nil? && !pred[:actual].nil?
|
|
52
|
+
|
|
53
|
+
result = evaluate_surprise(
|
|
54
|
+
domain: pred[:domain],
|
|
55
|
+
predicted: pred[:predicted],
|
|
56
|
+
actual: pred[:actual],
|
|
57
|
+
valence: pred.fetch(:valence, :neutral)
|
|
58
|
+
)
|
|
59
|
+
events << result[:surprise_event] if result[:success]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
habituation_model.decay_all
|
|
63
|
+
tick_cooldowns
|
|
64
|
+
|
|
65
|
+
orienting_count = events.count { |e| e[:orienting] }
|
|
66
|
+
Legion::Logging.debug "[surprise] tick update: evaluated=#{events.size} orienting=#{orienting_count}"
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
success: true,
|
|
70
|
+
evaluated: events.size,
|
|
71
|
+
orienting_count: orienting_count,
|
|
72
|
+
events: events
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def surprise_stats(**)
|
|
77
|
+
stats = store.to_h
|
|
78
|
+
top = store.most_surprising(1).first
|
|
79
|
+
avg_mag = stats[:average_magnitude]
|
|
80
|
+
|
|
81
|
+
Legion::Logging.debug "[surprise] stats: total=#{stats[:total_events]} domains=#{stats[:domain_count]}"
|
|
82
|
+
|
|
83
|
+
{
|
|
84
|
+
success: true,
|
|
85
|
+
total_events: stats[:total_events],
|
|
86
|
+
domain_count: stats[:domain_count],
|
|
87
|
+
average_magnitude: avg_mag,
|
|
88
|
+
most_surprising_domain: stats[:most_surprising_domain],
|
|
89
|
+
top_surprise_magnitude: top&.magnitude&.round(4)
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def domain_sensitivity(domain:, **)
|
|
94
|
+
sensitivity = habituation_model.sensitivity_for(domain)
|
|
95
|
+
baseline = store.baseline_for(domain)
|
|
96
|
+
domain_events = store.by_domain(domain)
|
|
97
|
+
|
|
98
|
+
Legion::Logging.debug "[surprise] domain_sensitivity: domain=#{domain} sensitivity=#{sensitivity.round(3)}"
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
success: true,
|
|
102
|
+
domain: domain,
|
|
103
|
+
sensitivity: sensitivity.round(4),
|
|
104
|
+
baseline: baseline.round(4),
|
|
105
|
+
event_count: domain_events.size
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def recent_surprises(count: 10, **)
|
|
110
|
+
events = store.recent(count)
|
|
111
|
+
Legion::Logging.debug "[surprise] recent_surprises: count=#{events.size}"
|
|
112
|
+
{ success: true, events: events.map(&:to_h), count: events.size }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def reset_habituation(domain:, **)
|
|
116
|
+
old_sensitivity = habituation_model.sensitivity_for(domain)
|
|
117
|
+
# Sensitize repeatedly to push back toward 1.0
|
|
118
|
+
steps = ((1.0 - old_sensitivity) / Helpers::Constants::SENSITIZATION_RATE).ceil
|
|
119
|
+
steps.times { habituation_model.sensitize(domain) }
|
|
120
|
+
new_sensitivity = habituation_model.sensitivity_for(domain)
|
|
121
|
+
|
|
122
|
+
Legion::Logging.debug "[surprise] reset_habituation: domain=#{domain} #{old_sensitivity.round(3)} -> #{new_sensitivity.round(3)}"
|
|
123
|
+
|
|
124
|
+
{ success: true, domain: domain, old_sensitivity: old_sensitivity.round(4), new_sensitivity: new_sensitivity.round(4) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def store
|
|
130
|
+
@store ||= Helpers::SurpriseStore.new
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def habituation_model
|
|
134
|
+
@habituation_model ||= Helpers::HabituationModel.new
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def cooldowns
|
|
138
|
+
@cooldowns ||= {}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def tick_cooldowns
|
|
142
|
+
cooldowns.each_key { |domain| cooldowns[domain] -= 1 }
|
|
143
|
+
cooldowns.reject! { |_, ticks_left| ticks_left <= 0 }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def record_cooldown(domain)
|
|
147
|
+
cooldowns[domain] = Helpers::Constants::ORIENTING_COOLDOWN
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def on_cooldown?(domain)
|
|
151
|
+
(cooldowns[domain] || 0).positive?
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def should_orient?(domain, magnitude, threshold)
|
|
155
|
+
return false if magnitude < threshold
|
|
156
|
+
return false if on_cooldown?(domain)
|
|
157
|
+
|
|
158
|
+
true
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def extract_predictions(tick_result)
|
|
162
|
+
return [] unless tick_result.is_a?(Hash)
|
|
163
|
+
|
|
164
|
+
tick_result.fetch(:predictions, [])
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'surprise/version'
|
|
4
|
+
require_relative 'surprise/helpers/constants'
|
|
5
|
+
require_relative 'surprise/helpers/surprise_event'
|
|
6
|
+
require_relative 'surprise/helpers/habituation_model'
|
|
7
|
+
require_relative 'surprise/helpers/surprise_store'
|
|
8
|
+
require_relative 'surprise/runners/surprise'
|
|
9
|
+
require_relative 'surprise/client'
|
|
10
|
+
|
|
11
|
+
module Legion
|
|
12
|
+
module Extensions
|
|
13
|
+
module Surprise
|
|
14
|
+
extend Legion::Extensions::Core if defined?(Legion::Extensions::Core)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-surprise
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Matthew Iverson
|
|
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: Detects when reality deviates from expectations using Bayesian surprise
|
|
27
|
+
(KL divergence). Tracks habituation, generates orienting signals, and adapts sensitivity
|
|
28
|
+
per domain.
|
|
29
|
+
email:
|
|
30
|
+
- matt@legionIO.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- lib/legion/extensions/surprise.rb
|
|
36
|
+
- lib/legion/extensions/surprise/client.rb
|
|
37
|
+
- lib/legion/extensions/surprise/helpers/constants.rb
|
|
38
|
+
- lib/legion/extensions/surprise/helpers/habituation_model.rb
|
|
39
|
+
- lib/legion/extensions/surprise/helpers/surprise_event.rb
|
|
40
|
+
- lib/legion/extensions/surprise/helpers/surprise_store.rb
|
|
41
|
+
- lib/legion/extensions/surprise/runners/surprise.rb
|
|
42
|
+
- lib/legion/extensions/surprise/version.rb
|
|
43
|
+
homepage: https://github.com/LegionIO/lex-surprise
|
|
44
|
+
licenses:
|
|
45
|
+
- MIT
|
|
46
|
+
metadata:
|
|
47
|
+
rubygems_mfa_required: 'true'
|
|
48
|
+
rdoc_options: []
|
|
49
|
+
require_paths:
|
|
50
|
+
- lib
|
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '3.4'
|
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
requirements: []
|
|
62
|
+
rubygems_version: 3.6.9
|
|
63
|
+
specification_version: 4
|
|
64
|
+
summary: Orienting response and surprise detection for LegionIO cognitive agents
|
|
65
|
+
test_files: []
|