lex-inner-speech 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: 4ab3daf8dea07c9c953b782bc0cddbffdd06170d128db9723ec9388aa58892a0
4
+ data.tar.gz: 762c31cd6ab0f38bcfad47a540b9900782f022448b14778a74bfa9a52b3da5ae
5
+ SHA512:
6
+ metadata.gz: 298a5228471e0fcd7557f13429ed0d8245ffa9b09377ace575e0643fde1c41fd0b5756d9f4657d0fa513bc720ea15b9caa76aa7e699d9b46f390f9a5bc57cb4a
7
+ data.tar.gz: 3fb165100c57f08b2ba4862d450b60eb6465ce09531c25b70c59279960ca84f09d690bd04b6a035f333899cc3468b7fde04c38e1dcdf4312363592b5e0321064
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module InnerSpeech
6
+ class Client
7
+ include Runners::InnerSpeech
8
+
9
+ def initialize(voice: nil)
10
+ @voice = voice || Helpers::InnerVoice.new
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module InnerSpeech
6
+ module Helpers
7
+ module Constants
8
+ MAX_UTTERANCES = 500
9
+ MAX_STREAM_LENGTH = 100
10
+ MAX_HISTORY = 200
11
+
12
+ # Speech condensation: inner speech is ~3x compressed vs external
13
+ CONDENSATION_RATIO = 0.33
14
+
15
+ # Rumination detection: same topic repeated N+ times
16
+ RUMINATION_THRESHOLD = 3
17
+
18
+ # Speed constants (utterances per tick)
19
+ AUTOMATIC_SPEED = 3
20
+ CONTROLLED_SPEED = 1
21
+ EGOCENTRIC_SPEED = 0.5
22
+
23
+ # Decay: how quickly old utterances lose salience
24
+ SALIENCE_DECAY = 0.05
25
+ SALIENCE_FLOOR = 0.01
26
+
27
+ SPEECH_MODES = %i[
28
+ planning rehearsal monitoring evaluating
29
+ questioning affirming narrating debating
30
+ comforting warning remembering imagining
31
+ ].freeze
32
+
33
+ VOICE_TYPES = %i[
34
+ rational emotional cautious bold
35
+ critical supportive curious skeptical
36
+ ].freeze
37
+
38
+ URGENCY_LABELS = {
39
+ (0.8..) => :critical,
40
+ (0.6...0.8) => :high,
41
+ (0.4...0.6) => :moderate,
42
+ (0.2...0.4) => :low,
43
+ (..0.2) => :background
44
+ }.freeze
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module InnerSpeech
6
+ module Helpers
7
+ class InnerVoice
8
+ include Constants
9
+
10
+ attr_reader :stream, :voices, :active_voice, :history
11
+
12
+ def initialize
13
+ @stream = SpeechStream.new
14
+ @voices = VOICE_TYPES.dup
15
+ @active_voice = :rational
16
+ @history = []
17
+ @speed = CONTROLLED_SPEED
18
+ end
19
+
20
+ def speak(content:, mode: :narrating, topic: :general, **)
21
+ utterance = @stream.append(
22
+ content: content,
23
+ mode: mode,
24
+ voice: @active_voice,
25
+ topic: topic,
26
+ **
27
+ )
28
+ record_event(:speak, utterance_id: utterance&.id) if utterance
29
+ utterance
30
+ end
31
+
32
+ def switch_voice(voice:)
33
+ sym = voice.to_sym
34
+ return nil unless VOICE_TYPES.include?(sym)
35
+
36
+ old_voice = @active_voice
37
+ @active_voice = sym
38
+ record_event(:switch_voice, from: old_voice, to: sym)
39
+ @active_voice
40
+ end
41
+
42
+ def plan(content:, topic: :general, **)
43
+ speak(content: content, mode: :planning, topic: topic, **)
44
+ end
45
+
46
+ def rehearse(content:, topic: :general, **)
47
+ speak(content: content, mode: :rehearsal, topic: topic, **)
48
+ end
49
+
50
+ def question(content:, topic: :general, **)
51
+ speak(content: content, mode: :questioning, topic: topic, **)
52
+ end
53
+
54
+ def evaluate(content:, topic: :general, **)
55
+ speak(content: content, mode: :evaluating, topic: topic, **)
56
+ end
57
+
58
+ def warn(content:, topic: :general, **)
59
+ speak(content: content, mode: :warning, topic: topic, urgency: 0.8, **)
60
+ end
61
+
62
+ def debate(content_a:, content_b:, topic: :general, **)
63
+ old_voice = @active_voice
64
+ @active_voice = :bold
65
+ utt_a = speak(content: content_a, mode: :debating, topic: topic, **)
66
+ @active_voice = :cautious
67
+ utt_b = speak(content: content_b, mode: :debating, topic: topic, **)
68
+ @active_voice = old_voice
69
+ [utt_a, utt_b].compact
70
+ end
71
+
72
+ def interrupt(content:, **)
73
+ utterance = @stream.interrupt(content: content, **)
74
+ record_event(:interrupt, utterance_id: utterance&.id) if utterance
75
+ utterance
76
+ end
77
+
78
+ SPEED_MAP = {
79
+ automatic: AUTOMATIC_SPEED,
80
+ controlled: CONTROLLED_SPEED,
81
+ egocentric: EGOCENTRIC_SPEED
82
+ }.freeze
83
+
84
+ def set_speed(mode:)
85
+ @speed = SPEED_MAP.fetch(mode, CONTROLLED_SPEED)
86
+ end
87
+
88
+ def ruminating?
89
+ @stream.ruminating?
90
+ end
91
+
92
+ def break_rumination(redirect_topic:)
93
+ return false unless ruminating?
94
+
95
+ speak(content: 'Let me think about something else.', mode: :narrating, topic: redirect_topic)
96
+ true
97
+ end
98
+
99
+ def recent_speech(count: 5)
100
+ @stream.recent(count: count)
101
+ end
102
+
103
+ def narrative
104
+ @stream.narrative
105
+ end
106
+
107
+ def condensed_narrative
108
+ @stream.condensed_stream.join(' ')
109
+ end
110
+
111
+ def tick
112
+ @stream.decay_all
113
+ @stream.to_h.merge(active_voice: @active_voice, speed: @speed)
114
+ end
115
+
116
+ def to_h
117
+ {
118
+ active_voice: @active_voice,
119
+ speed: @speed,
120
+ stream_size: @stream.size,
121
+ ruminating: ruminating?,
122
+ total_utterances: @stream.counter,
123
+ history_size: @history.size
124
+ }
125
+ end
126
+
127
+ private
128
+
129
+ def record_event(type, **details)
130
+ @history << { type: type, at: Time.now.utc }.merge(details)
131
+ @history.shift while @history.size > MAX_HISTORY
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module InnerSpeech
6
+ module Helpers
7
+ class SpeechStream
8
+ include Constants
9
+
10
+ attr_reader :utterances, :counter
11
+
12
+ def initialize
13
+ @utterances = []
14
+ @counter = 0
15
+ end
16
+
17
+ def append(content:, mode: :narrating, **)
18
+ return nil if @utterances.size >= MAX_STREAM_LENGTH
19
+
20
+ @counter += 1
21
+ utterance = Utterance.new(
22
+ id: :"utt_#{@counter}",
23
+ content: content,
24
+ mode: mode,
25
+ **
26
+ )
27
+ @utterances << utterance
28
+ utterance
29
+ end
30
+
31
+ def current
32
+ @utterances.last
33
+ end
34
+
35
+ def recent(count: 5)
36
+ @utterances.last(count).map(&:to_h)
37
+ end
38
+
39
+ def by_mode(mode:)
40
+ @utterances.select { |u| u.mode == mode }.map(&:to_h)
41
+ end
42
+
43
+ def by_voice(voice:)
44
+ @utterances.select { |u| u.voice == voice }.map(&:to_h)
45
+ end
46
+
47
+ def by_topic(topic:)
48
+ @utterances.select { |u| u.topic == topic }.map(&:to_h)
49
+ end
50
+
51
+ def salient
52
+ @utterances.select(&:salient?).map(&:to_h)
53
+ end
54
+
55
+ def urgent
56
+ @utterances.select(&:urgent?).map(&:to_h)
57
+ end
58
+
59
+ def ruminating?
60
+ return false if @utterances.size < RUMINATION_THRESHOLD
61
+
62
+ recent_topics = @utterances.last(RUMINATION_THRESHOLD).map(&:topic)
63
+ recent_topics.uniq.size == 1
64
+ end
65
+
66
+ def rumination_topic
67
+ return nil unless ruminating?
68
+
69
+ @utterances.last&.topic
70
+ end
71
+
72
+ def decay_all
73
+ @utterances.each(&:decay_salience!)
74
+ prune_stale
75
+ end
76
+
77
+ def interrupt(content:, mode: :warning, **)
78
+ append(content: content, mode: mode, urgency: 0.9, salience: 0.9, **)
79
+ end
80
+
81
+ def condensed_stream
82
+ @utterances.select(&:salient?).map(&:condensed_content)
83
+ end
84
+
85
+ def narrative
86
+ @utterances.map(&:content).join(' ')
87
+ end
88
+
89
+ def clear
90
+ @utterances.clear
91
+ end
92
+
93
+ def size
94
+ @utterances.size
95
+ end
96
+
97
+ def to_h
98
+ {
99
+ size: @utterances.size,
100
+ total_generated: @counter,
101
+ ruminating: ruminating?,
102
+ rumination_topic: rumination_topic,
103
+ salient_count: @utterances.count(&:salient?),
104
+ urgent_count: @utterances.count(&:urgent?),
105
+ mode_distribution: mode_distribution
106
+ }
107
+ end
108
+
109
+ private
110
+
111
+ def prune_stale
112
+ @utterances.reject! { |u| u.salience <= SALIENCE_FLOOR }
113
+ end
114
+
115
+ def mode_distribution
116
+ dist = Hash.new(0)
117
+ @utterances.each { |u| dist[u.mode] += 1 }
118
+ dist
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module InnerSpeech
6
+ module Helpers
7
+ class Utterance
8
+ include Constants
9
+
10
+ attr_reader :id, :content, :mode, :voice, :topic,
11
+ :urgency, :salience, :source_subsystem, :created_at
12
+
13
+ def initialize(id:, content:, mode: :narrating, **opts)
14
+ @id = id
15
+ @content = content
16
+ @mode = resolve_mode(mode)
17
+ apply_opts(opts)
18
+ @created_at = Time.now.utc
19
+ end
20
+
21
+ def condensed_content
22
+ words = @content.split
23
+ keep = [(words.size * CONDENSATION_RATIO).ceil, 1].max
24
+ words.first(keep).join(' ')
25
+ end
26
+
27
+ def urgent?
28
+ @urgency >= 0.6
29
+ end
30
+
31
+ def background?
32
+ @urgency < 0.2
33
+ end
34
+
35
+ def decay_salience!
36
+ @salience = [@salience - SALIENCE_DECAY, SALIENCE_FLOOR].max
37
+ end
38
+
39
+ def salient?
40
+ @salience >= 0.3
41
+ end
42
+
43
+ def urgency_label
44
+ URGENCY_LABELS.each { |range, lbl| return lbl if range.cover?(@urgency) }
45
+ :background
46
+ end
47
+
48
+ def to_h
49
+ {
50
+ id: @id,
51
+ content: @content,
52
+ condensed: condensed_content,
53
+ mode: @mode,
54
+ voice: @voice,
55
+ topic: @topic,
56
+ urgency: @urgency.round(4),
57
+ salience: @salience.round(4),
58
+ source_subsystem: @source_subsystem,
59
+ urgency_label: urgency_label
60
+ }
61
+ end
62
+
63
+ private
64
+
65
+ def apply_opts(opts)
66
+ @voice = resolve_voice(opts.fetch(:voice, :rational))
67
+ @topic = opts.fetch(:topic, :general)
68
+ @urgency = opts.fetch(:urgency, 0.5).to_f.clamp(0.0, 1.0)
69
+ @salience = opts.fetch(:salience, 0.5).to_f.clamp(0.0, 1.0)
70
+ @source_subsystem = opts.fetch(:source_subsystem, :unknown)
71
+ end
72
+
73
+ def resolve_mode(mode)
74
+ sym = mode.to_sym
75
+ SPEECH_MODES.include?(sym) ? sym : :narrating
76
+ end
77
+
78
+ def resolve_voice(voice)
79
+ sym = voice.to_sym
80
+ VOICE_TYPES.include?(sym) ? sym : :rational
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module InnerSpeech
6
+ module Runners
7
+ module InnerSpeech
8
+ include Helpers::Constants
9
+ include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
10
+
11
+ def inner_speak(content:, mode: :narrating, topic: :general, **)
12
+ utterance = voice.speak(content: content, mode: mode, topic: topic)
13
+ return { success: false, reason: :stream_full } unless utterance
14
+
15
+ { success: true, utterance_id: utterance.id, mode: utterance.mode }
16
+ end
17
+
18
+ def inner_plan(content:, topic: :general, **)
19
+ utterance = voice.plan(content: content, topic: topic)
20
+ return { success: false, reason: :stream_full } unless utterance
21
+
22
+ { success: true, utterance_id: utterance.id }
23
+ end
24
+
25
+ def inner_question(content:, topic: :general, **)
26
+ utterance = voice.question(content: content, topic: topic)
27
+ return { success: false, reason: :stream_full } unless utterance
28
+
29
+ { success: true, utterance_id: utterance.id }
30
+ end
31
+
32
+ def inner_debate(content_a:, content_b:, topic: :general, **)
33
+ results = voice.debate(content_a: content_a, content_b: content_b, topic: topic)
34
+ { success: true, utterances: results.map(&:id), count: results.size }
35
+ end
36
+
37
+ def switch_inner_voice(voice_type:, **)
38
+ result = voice.switch_voice(voice: voice_type)
39
+ return { success: false, reason: :invalid_voice } unless result
40
+
41
+ { success: true, active_voice: result }
42
+ end
43
+
44
+ def inner_interrupt(content:, **)
45
+ utterance = voice.interrupt(content: content)
46
+ return { success: false, reason: :stream_full } unless utterance
47
+
48
+ { success: true, utterance_id: utterance.id }
49
+ end
50
+
51
+ def break_inner_rumination(redirect_topic: :general, **)
52
+ result = voice.break_rumination(redirect_topic: redirect_topic)
53
+ { success: true, rumination_broken: result }
54
+ end
55
+
56
+ def recent_inner_speech(count: 5, **)
57
+ speech = voice.recent_speech(count: count)
58
+ { success: true, utterances: speech, count: speech.size }
59
+ end
60
+
61
+ def inner_narrative(**)
62
+ { success: true, narrative: voice.narrative, condensed: voice.condensed_narrative }
63
+ end
64
+
65
+ def update_inner_speech(**)
66
+ result = voice.tick
67
+ { success: true }.merge(result)
68
+ end
69
+
70
+ def inner_speech_stats(**)
71
+ { success: true }.merge(voice.to_h)
72
+ end
73
+
74
+ private
75
+
76
+ def voice
77
+ @voice ||= Helpers::InnerVoice.new
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module InnerSpeech
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-inner-speech
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: Vygotsky inner speech for LegionIO — internal monologue, dialogic voices,
27
+ rumination detection, and speech condensation
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/legion/extensions/inner_speech/client.rb
35
+ - lib/legion/extensions/inner_speech/helpers/constants.rb
36
+ - lib/legion/extensions/inner_speech/helpers/inner_voice.rb
37
+ - lib/legion/extensions/inner_speech/helpers/speech_stream.rb
38
+ - lib/legion/extensions/inner_speech/helpers/utterance.rb
39
+ - lib/legion/extensions/inner_speech/runners/inner_speech.rb
40
+ - lib/legion/extensions/inner_speech/version.rb
41
+ homepage: https://github.com/LegionIO/lex-inner-speech
42
+ licenses:
43
+ - MIT
44
+ metadata:
45
+ homepage_uri: https://github.com/LegionIO/lex-inner-speech
46
+ source_code_uri: https://github.com/LegionIO/lex-inner-speech
47
+ documentation_uri: https://github.com/LegionIO/lex-inner-speech/blob/master/README.md
48
+ changelog_uri: https://github.com/LegionIO/lex-inner-speech/blob/master/CHANGELOG.md
49
+ bug_tracker_uri: https://github.com/LegionIO/lex-inner-speech/issues
50
+ rubygems_mfa_required: 'true'
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '3.4'
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubygems_version: 3.6.9
66
+ specification_version: 4
67
+ summary: Inner speech engine for LegionIO
68
+ test_files: []