lex-agentic-language 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.
Files changed (126) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/Gemfile +5 -0
  4. data/LICENSE +21 -0
  5. data/README.md +13 -0
  6. data/lex-agentic-language.gemspec +30 -0
  7. data/lib/legion/extensions/agentic/language/conceptual_blending/client.rb +25 -0
  8. data/lib/legion/extensions/agentic/language/conceptual_blending/helpers/blend.rb +91 -0
  9. data/lib/legion/extensions/agentic/language/conceptual_blending/helpers/blending_engine.rb +171 -0
  10. data/lib/legion/extensions/agentic/language/conceptual_blending/helpers/constants.rb +35 -0
  11. data/lib/legion/extensions/agentic/language/conceptual_blending/helpers/mental_space.rb +51 -0
  12. data/lib/legion/extensions/agentic/language/conceptual_blending/runners/conceptual_blending.rb +106 -0
  13. data/lib/legion/extensions/agentic/language/conceptual_blending/version.rb +13 -0
  14. data/lib/legion/extensions/agentic/language/conceptual_blending.rb +20 -0
  15. data/lib/legion/extensions/agentic/language/conceptual_metaphor/client.rb +19 -0
  16. data/lib/legion/extensions/agentic/language/conceptual_metaphor/helpers/constants.rb +49 -0
  17. data/lib/legion/extensions/agentic/language/conceptual_metaphor/helpers/metaphor.rb +109 -0
  18. data/lib/legion/extensions/agentic/language/conceptual_metaphor/helpers/metaphor_engine.rb +154 -0
  19. data/lib/legion/extensions/agentic/language/conceptual_metaphor/runners/conceptual_metaphor.rb +107 -0
  20. data/lib/legion/extensions/agentic/language/conceptual_metaphor/version.rb +13 -0
  21. data/lib/legion/extensions/agentic/language/conceptual_metaphor.rb +19 -0
  22. data/lib/legion/extensions/agentic/language/frame_semantics/helpers/client.rb +23 -0
  23. data/lib/legion/extensions/agentic/language/frame_semantics/helpers/constants.rb +32 -0
  24. data/lib/legion/extensions/agentic/language/frame_semantics/helpers/frame.rb +109 -0
  25. data/lib/legion/extensions/agentic/language/frame_semantics/helpers/frame_engine.rb +139 -0
  26. data/lib/legion/extensions/agentic/language/frame_semantics/helpers/frame_instance.rb +51 -0
  27. data/lib/legion/extensions/agentic/language/frame_semantics/runners/frame_semantics.rb +108 -0
  28. data/lib/legion/extensions/agentic/language/frame_semantics/version.rb +13 -0
  29. data/lib/legion/extensions/agentic/language/frame_semantics.rb +20 -0
  30. data/lib/legion/extensions/agentic/language/grammar/client.rb +29 -0
  31. data/lib/legion/extensions/agentic/language/grammar/helpers/constants.rb +38 -0
  32. data/lib/legion/extensions/agentic/language/grammar/helpers/construal.rb +68 -0
  33. data/lib/legion/extensions/agentic/language/grammar/helpers/construction.rb +68 -0
  34. data/lib/legion/extensions/agentic/language/grammar/helpers/grammar_engine.rb +119 -0
  35. data/lib/legion/extensions/agentic/language/grammar/runners/cognitive_grammar.rb +100 -0
  36. data/lib/legion/extensions/agentic/language/grammar/version.rb +13 -0
  37. data/lib/legion/extensions/agentic/language/grammar.rb +20 -0
  38. data/lib/legion/extensions/agentic/language/inner_speech/client.rb +19 -0
  39. data/lib/legion/extensions/agentic/language/inner_speech/helpers/constants.rb +53 -0
  40. data/lib/legion/extensions/agentic/language/inner_speech/helpers/inner_voice.rb +141 -0
  41. data/lib/legion/extensions/agentic/language/inner_speech/helpers/speech_stream.rb +128 -0
  42. data/lib/legion/extensions/agentic/language/inner_speech/helpers/utterance.rb +90 -0
  43. data/lib/legion/extensions/agentic/language/inner_speech/runners/inner_speech.rb +87 -0
  44. data/lib/legion/extensions/agentic/language/inner_speech/version.rb +13 -0
  45. data/lib/legion/extensions/agentic/language/inner_speech.rb +20 -0
  46. data/lib/legion/extensions/agentic/language/language/client.rb +26 -0
  47. data/lib/legion/extensions/agentic/language/language/helpers/constants.rb +43 -0
  48. data/lib/legion/extensions/agentic/language/language/helpers/lexicon.rb +63 -0
  49. data/lib/legion/extensions/agentic/language/language/helpers/summarizer.rb +167 -0
  50. data/lib/legion/extensions/agentic/language/language/runners/language.rb +134 -0
  51. data/lib/legion/extensions/agentic/language/language/version.rb +13 -0
  52. data/lib/legion/extensions/agentic/language/language.rb +19 -0
  53. data/lib/legion/extensions/agentic/language/narrative_reasoning/client.rb +28 -0
  54. data/lib/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative.rb +123 -0
  55. data/lib/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_engine.rb +122 -0
  56. data/lib/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_event.rb +41 -0
  57. data/lib/legion/extensions/agentic/language/narrative_reasoning/runners/narrative_reasoning.rb +122 -0
  58. data/lib/legion/extensions/agentic/language/narrative_reasoning/version.rb +13 -0
  59. data/lib/legion/extensions/agentic/language/narrative_reasoning.rb +18 -0
  60. data/lib/legion/extensions/agentic/language/narrator/client.rb +27 -0
  61. data/lib/legion/extensions/agentic/language/narrator/helpers/constants.rb +69 -0
  62. data/lib/legion/extensions/agentic/language/narrator/helpers/journal.rb +68 -0
  63. data/lib/legion/extensions/agentic/language/narrator/helpers/llm_enhancer.rb +105 -0
  64. data/lib/legion/extensions/agentic/language/narrator/helpers/prose.rb +122 -0
  65. data/lib/legion/extensions/agentic/language/narrator/helpers/synthesizer.rb +138 -0
  66. data/lib/legion/extensions/agentic/language/narrator/runners/narrator.rb +196 -0
  67. data/lib/legion/extensions/agentic/language/narrator/version.rb +13 -0
  68. data/lib/legion/extensions/agentic/language/narrator.rb +21 -0
  69. data/lib/legion/extensions/agentic/language/pragmatic_inference/client.rb +28 -0
  70. data/lib/legion/extensions/agentic/language/pragmatic_inference/helpers/constants.rb +52 -0
  71. data/lib/legion/extensions/agentic/language/pragmatic_inference/helpers/pragmatic_engine.rb +164 -0
  72. data/lib/legion/extensions/agentic/language/pragmatic_inference/helpers/utterance.rb +84 -0
  73. data/lib/legion/extensions/agentic/language/pragmatic_inference/runners/pragmatic_inference.rb +136 -0
  74. data/lib/legion/extensions/agentic/language/pragmatic_inference/version.rb +13 -0
  75. data/lib/legion/extensions/agentic/language/pragmatic_inference.rb +18 -0
  76. data/lib/legion/extensions/agentic/language/version.rb +11 -0
  77. data/lib/legion/extensions/agentic/language.rb +28 -0
  78. data/spec/legion/extensions/agentic/language/conceptual_blending/client_spec.rb +78 -0
  79. data/spec/legion/extensions/agentic/language/conceptual_blending/helpers/blend_spec.rb +141 -0
  80. data/spec/legion/extensions/agentic/language/conceptual_blending/helpers/blending_engine_spec.rb +211 -0
  81. data/spec/legion/extensions/agentic/language/conceptual_blending/helpers/mental_space_spec.rb +85 -0
  82. data/spec/legion/extensions/agentic/language/conceptual_blending/runners/conceptual_blending_spec.rb +162 -0
  83. data/spec/legion/extensions/agentic/language/conceptual_metaphor/client_spec.rb +29 -0
  84. data/spec/legion/extensions/agentic/language/conceptual_metaphor/helpers/metaphor_engine_spec.rb +166 -0
  85. data/spec/legion/extensions/agentic/language/conceptual_metaphor/helpers/metaphor_spec.rb +133 -0
  86. data/spec/legion/extensions/agentic/language/conceptual_metaphor/runners/conceptual_metaphor_spec.rb +133 -0
  87. data/spec/legion/extensions/agentic/language/frame_semantics/helpers/frame_engine_spec.rb +227 -0
  88. data/spec/legion/extensions/agentic/language/frame_semantics/helpers/frame_instance_spec.rb +83 -0
  89. data/spec/legion/extensions/agentic/language/frame_semantics/helpers/frame_spec.rb +213 -0
  90. data/spec/legion/extensions/agentic/language/frame_semantics/runners/frame_semantics_spec.rb +155 -0
  91. data/spec/legion/extensions/agentic/language/grammar/client_spec.rb +121 -0
  92. data/spec/legion/extensions/agentic/language/grammar/cognitive_grammar_spec.rb +18 -0
  93. data/spec/legion/extensions/agentic/language/grammar/helpers/constants_spec.rb +67 -0
  94. data/spec/legion/extensions/agentic/language/grammar/helpers/construal_spec.rb +124 -0
  95. data/spec/legion/extensions/agentic/language/grammar/helpers/construction_spec.rb +155 -0
  96. data/spec/legion/extensions/agentic/language/grammar/helpers/grammar_engine_spec.rb +206 -0
  97. data/spec/legion/extensions/agentic/language/grammar/runners/cognitive_grammar_spec.rb +189 -0
  98. data/spec/legion/extensions/agentic/language/inner_speech/client_spec.rb +39 -0
  99. data/spec/legion/extensions/agentic/language/inner_speech/helpers/inner_voice_spec.rb +185 -0
  100. data/spec/legion/extensions/agentic/language/inner_speech/helpers/speech_stream_spec.rb +158 -0
  101. data/spec/legion/extensions/agentic/language/inner_speech/helpers/utterance_spec.rb +121 -0
  102. data/spec/legion/extensions/agentic/language/inner_speech/runners/inner_speech_spec.rb +102 -0
  103. data/spec/legion/extensions/agentic/language/language/client_spec.rb +20 -0
  104. data/spec/legion/extensions/agentic/language/language/helpers/constants_spec.rb +31 -0
  105. data/spec/legion/extensions/agentic/language/language/helpers/lexicon_spec.rb +116 -0
  106. data/spec/legion/extensions/agentic/language/language/helpers/summarizer_spec.rb +224 -0
  107. data/spec/legion/extensions/agentic/language/language/runners/language_spec.rb +169 -0
  108. data/spec/legion/extensions/agentic/language/narrative_reasoning/client_spec.rb +19 -0
  109. data/spec/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_engine_spec.rb +182 -0
  110. data/spec/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_event_spec.rb +61 -0
  111. data/spec/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_spec.rb +168 -0
  112. data/spec/legion/extensions/agentic/language/narrative_reasoning/runners/narrative_reasoning_spec.rb +174 -0
  113. data/spec/legion/extensions/agentic/language/narrator/client_spec.rb +24 -0
  114. data/spec/legion/extensions/agentic/language/narrator/helpers/journal_spec.rb +95 -0
  115. data/spec/legion/extensions/agentic/language/narrator/helpers/llm_enhancer_spec.rb +107 -0
  116. data/spec/legion/extensions/agentic/language/narrator/helpers/prose_spec.rb +134 -0
  117. data/spec/legion/extensions/agentic/language/narrator/helpers/synthesizer_spec.rb +89 -0
  118. data/spec/legion/extensions/agentic/language/narrator/runners/narrator_llm_spec.rb +74 -0
  119. data/spec/legion/extensions/agentic/language/narrator/runners/narrator_spec.rb +126 -0
  120. data/spec/legion/extensions/agentic/language/pragmatic_inference/client_spec.rb +19 -0
  121. data/spec/legion/extensions/agentic/language/pragmatic_inference/helpers/constants_spec.rb +73 -0
  122. data/spec/legion/extensions/agentic/language/pragmatic_inference/helpers/pragmatic_engine_spec.rb +185 -0
  123. data/spec/legion/extensions/agentic/language/pragmatic_inference/helpers/utterance_spec.rb +111 -0
  124. data/spec/legion/extensions/agentic/language/pragmatic_inference/runners/pragmatic_inference_spec.rb +231 -0
  125. data/spec/spec_helper.rb +33 -0
  126. metadata +210 -0
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Language::Narrator::Helpers::Journal do
4
+ subject(:journal) { described_class.new }
5
+
6
+ let(:entry) do
7
+ { timestamp: Time.now.utc, narrative: 'I am alert.', mood: :energized, sections: {} }
8
+ end
9
+
10
+ describe '#append' do
11
+ it 'adds an entry' do
12
+ journal.append(entry)
13
+ expect(journal.size).to eq(1)
14
+ end
15
+
16
+ it 'returns the appended entry' do
17
+ result = journal.append(entry)
18
+ expect(result[:narrative]).to eq('I am alert.')
19
+ end
20
+
21
+ it 'trims to MAX_JOURNAL_SIZE' do
22
+ max = Legion::Extensions::Agentic::Language::Narrator::Helpers::Constants::MAX_JOURNAL_SIZE
23
+ (max + 10).times { |i| journal.append(entry.merge(narrative: "Entry #{i}")) }
24
+ expect(journal.size).to eq(max)
25
+ end
26
+ end
27
+
28
+ describe '#recent' do
29
+ it 'returns last N entries' do
30
+ 5.times { |i| journal.append(entry.merge(narrative: "Entry #{i}")) }
31
+ result = journal.recent(limit: 3)
32
+ expect(result.size).to eq(3)
33
+ expect(result.last[:narrative]).to eq('Entry 4')
34
+ end
35
+
36
+ it 'returns all entries when fewer than limit' do
37
+ 2.times { |i| journal.append(entry.merge(narrative: "Entry #{i}")) }
38
+ expect(journal.recent(limit: 10).size).to eq(2)
39
+ end
40
+ end
41
+
42
+ describe '#since' do
43
+ it 'returns entries after a timestamp' do
44
+ old = entry.merge(timestamp: Time.now.utc - 3600)
45
+ recent_entry = entry.merge(timestamp: Time.now.utc)
46
+
47
+ journal.append(old)
48
+ journal.append(recent_entry)
49
+
50
+ cutoff = Time.now.utc - 1800
51
+ result = journal.since(cutoff)
52
+ expect(result.size).to eq(1)
53
+ end
54
+ end
55
+
56
+ describe '#by_mood' do
57
+ it 'filters entries by mood' do
58
+ journal.append(entry.merge(mood: :energized))
59
+ journal.append(entry.merge(mood: :dormant))
60
+ journal.append(entry.merge(mood: :energized))
61
+
62
+ result = journal.by_mood(:energized)
63
+ expect(result.size).to eq(2)
64
+ end
65
+ end
66
+
67
+ describe '#stats' do
68
+ it 'returns empty stats for empty journal' do
69
+ stats = journal.stats
70
+ expect(stats[:total]).to eq(0)
71
+ expect(stats[:moods]).to eq({})
72
+ end
73
+
74
+ it 'returns mood distribution and timestamps' do
75
+ journal.append(entry.merge(mood: :energized))
76
+ journal.append(entry.merge(mood: :neutral))
77
+ journal.append(entry.merge(mood: :energized))
78
+
79
+ stats = journal.stats
80
+ expect(stats[:total]).to eq(3)
81
+ expect(stats[:moods][:energized]).to eq(2)
82
+ expect(stats[:moods][:neutral]).to eq(1)
83
+ expect(stats).to have_key(:oldest)
84
+ expect(stats).to have_key(:newest)
85
+ end
86
+ end
87
+
88
+ describe '#clear' do
89
+ it 'removes all entries' do
90
+ 3.times { journal.append(entry) }
91
+ journal.clear
92
+ expect(journal.size).to eq(0)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Language::Narrator::Helpers::LlmEnhancer do
4
+ describe '.available?' do
5
+ context 'when Legion::LLM is not defined' do
6
+ it 'returns false' do
7
+ hide_const('Legion::LLM')
8
+ expect(described_class.available?).to be false
9
+ end
10
+ end
11
+
12
+ context 'when Legion::LLM is defined but not started' do
13
+ it 'returns false' do
14
+ llm_double = double('Legion::LLM', started?: false)
15
+ stub_const('Legion::LLM', llm_double)
16
+ expect(described_class.available?).to be false
17
+ end
18
+ end
19
+
20
+ context 'when Legion::LLM is started' do
21
+ it 'returns true' do
22
+ llm_double = double('Legion::LLM', started?: true)
23
+ stub_const('Legion::LLM', llm_double)
24
+ expect(described_class.available?).to be true
25
+ end
26
+ end
27
+
28
+ context 'when Legion::LLM raises an error' do
29
+ it 'returns false' do
30
+ llm_double = double('Legion::LLM')
31
+ allow(llm_double).to receive(:respond_to?).and_raise(StandardError)
32
+ stub_const('Legion::LLM', llm_double)
33
+ expect(described_class.available?).to be false
34
+ end
35
+ end
36
+ end
37
+
38
+ describe '.narrate' do
39
+ let(:sections_data) do
40
+ {
41
+ emotion: { valence: 0.6, arousal: 0.7, gut: nil },
42
+ curiosity: { intensity: 0.5, wonder_count: 3, top_wonder: 'What is next?' },
43
+ prediction: { confidence: 0.8, pending: 2, mode: :causal },
44
+ memory: { trace_count: 42, health: 0.9 },
45
+ attention: { spotlight: 4, peripheral: 2, focused_domains: ['planning'] },
46
+ reflection: { health: 0.95, pending_adaptations: 1 }
47
+ }
48
+ end
49
+
50
+ context 'when LLM returns a response' do
51
+ it 'returns the response content as a string' do
52
+ response_double = double('response', content: 'I feel alert and curious about what lies ahead.')
53
+ chat_double = double('chat')
54
+ allow(chat_double).to receive(:with_instructions)
55
+ allow(chat_double).to receive(:ask).and_return(response_double)
56
+ llm_double = double('Legion::LLM', started?: true)
57
+ allow(llm_double).to receive(:chat).and_return(chat_double)
58
+ stub_const('Legion::LLM', llm_double)
59
+
60
+ result = described_class.narrate(sections_data: sections_data)
61
+ expect(result).to be_a(String)
62
+ expect(result).to eq('I feel alert and curious about what lies ahead.')
63
+ end
64
+ end
65
+
66
+ context 'when LLM returns nil response' do
67
+ it 'returns nil' do
68
+ chat_double = double('chat')
69
+ allow(chat_double).to receive(:with_instructions)
70
+ allow(chat_double).to receive(:ask).and_return(nil)
71
+ llm_double = double('Legion::LLM', started?: true)
72
+ allow(llm_double).to receive(:chat).and_return(chat_double)
73
+ stub_const('Legion::LLM', llm_double)
74
+
75
+ result = described_class.narrate(sections_data: sections_data)
76
+ expect(result).to be_nil
77
+ end
78
+ end
79
+
80
+ context 'when an error occurs' do
81
+ it 'returns nil and logs a warning' do
82
+ llm_double = double('Legion::LLM', started?: true)
83
+ allow(llm_double).to receive(:chat).and_raise(StandardError, 'connection failed')
84
+ stub_const('Legion::LLM', llm_double)
85
+
86
+ expect(Legion::Logging).to receive(:warn).with(/narrator:llm.*narrate failed/)
87
+ result = described_class.narrate(sections_data: sections_data)
88
+ expect(result).to be_nil
89
+ end
90
+ end
91
+
92
+ context 'with empty sections_data' do
93
+ it 'does not raise and returns a string when LLM responds' do
94
+ response_double = double('response', content: 'Everything seems quiet.')
95
+ chat_double = double('chat')
96
+ allow(chat_double).to receive(:with_instructions)
97
+ allow(chat_double).to receive(:ask).and_return(response_double)
98
+ llm_double = double('Legion::LLM', started?: true)
99
+ allow(llm_double).to receive(:chat).and_return(chat_double)
100
+ stub_const('Legion::LLM', llm_double)
101
+
102
+ result = described_class.narrate(sections_data: {})
103
+ expect(result).to eq('Everything seems quiet.')
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Language::Narrator::Helpers::Prose do
4
+ let(:prose) { described_class }
5
+
6
+ describe '.emotion_phrase' do
7
+ it 'describes high positive valence with high arousal' do
8
+ result = prose.emotion_phrase(valence: 0.8, arousal: 0.9)
9
+ expect(result).to include('highly alert')
10
+ expect(result).to include('engaged and optimistic')
11
+ end
12
+
13
+ it 'describes low negative valence' do
14
+ result = prose.emotion_phrase(valence: -0.4, arousal: 0.3)
15
+ expect(result).to include('slightly uneasy')
16
+ end
17
+
18
+ it 'includes gut signal when strong positive' do
19
+ result = prose.emotion_phrase(valence: 0.0, arousal: 0.5, gut: { signal: 0.5 })
20
+ expect(result).to include('something important')
21
+ end
22
+
23
+ it 'includes gut signal when strong negative' do
24
+ result = prose.emotion_phrase(valence: 0.0, arousal: 0.5, gut: { signal: -0.5 })
25
+ expect(result).to include('uneasy feeling')
26
+ end
27
+
28
+ it 'omits gut note when signal is mild' do
29
+ result = prose.emotion_phrase(valence: 0.0, arousal: 0.5, gut: { signal: 0.1 })
30
+ expect(result).not_to include('gut')
31
+ expect(result).not_to include('important')
32
+ end
33
+ end
34
+
35
+ describe '.curiosity_phrase' do
36
+ it 'describes high curiosity with wonders' do
37
+ result = prose.curiosity_phrase(intensity: 0.8, top_wonder: 'Why are traces sparse?', wonder_count: 3)
38
+ expect(result).to include('deeply curious')
39
+ expect(result).to include('3 open questions')
40
+ expect(result).to include('Why are traces sparse?')
41
+ end
42
+
43
+ it 'describes no curiosity' do
44
+ result = prose.curiosity_phrase(intensity: 0.0)
45
+ expect(result).to include('not particularly curious')
46
+ end
47
+
48
+ it 'handles single wonder' do
49
+ result = prose.curiosity_phrase(intensity: 0.5, top_wonder: 'What is this?', wonder_count: 1)
50
+ expect(result).to include('1 open question')
51
+ end
52
+ end
53
+
54
+ describe '.prediction_phrase' do
55
+ it 'describes high confidence' do
56
+ result = prose.prediction_phrase(confidence: 0.9, pending: 2, mode: :functional_mapping)
57
+ expect(result).to include('confident')
58
+ expect(result).to include('functional_mapping')
59
+ expect(result).to include('2 pending predictions')
60
+ end
61
+
62
+ it 'describes low confidence' do
63
+ result = prose.prediction_phrase(confidence: 0.2)
64
+ expect(result).to include('uncertain')
65
+ end
66
+ end
67
+
68
+ describe '.attention_phrase' do
69
+ it 'describes spotlight and peripheral counts' do
70
+ result = prose.attention_phrase(spotlight: 3, peripheral: 5)
71
+ expect(result).to include('3 signals in spotlight')
72
+ expect(result).to include('5 in peripheral')
73
+ end
74
+
75
+ it 'includes manual focus domains' do
76
+ result = prose.attention_phrase(spotlight: 1, peripheral: 0, focused_domains: %w[terraform vault])
77
+ expect(result).to include('terraform')
78
+ expect(result).to include('vault')
79
+ end
80
+ end
81
+
82
+ describe '.memory_phrase' do
83
+ it 'describes memory state' do
84
+ result = prose.memory_phrase(trace_count: 150, health: 0.85)
85
+ expect(result).to include('150 active traces')
86
+ expect(result).to include('functioning well')
87
+ end
88
+ end
89
+
90
+ describe '.reflection_phrase' do
91
+ it 'describes healthy state' do
92
+ result = prose.reflection_phrase(health: 0.95)
93
+ expect(result).to include('operating at full capacity')
94
+ end
95
+
96
+ it 'includes pending adaptations' do
97
+ result = prose.reflection_phrase(health: 0.6, pending_adaptations: 3, recent_severity: :significant)
98
+ expect(result).to include('3 pending adaptation')
99
+ expect(result).to include('significant')
100
+ end
101
+ end
102
+
103
+ describe '.overall_narrative' do
104
+ it 'joins sections with periods' do
105
+ result = prose.overall_narrative(['I am alert', 'I am curious', 'Memory is good'])
106
+ expect(result).to eq('I am alert. I am curious. Memory is good.')
107
+ end
108
+
109
+ it 'skips empty sections' do
110
+ result = prose.overall_narrative(['I am alert', '', nil, 'Memory is good'])
111
+ expect(result).to eq('I am alert. Memory is good.')
112
+ end
113
+ end
114
+
115
+ describe '.emotion_label_for' do
116
+ it 'returns correct labels for valence ranges' do
117
+ expect(prose.emotion_label_for(0.8)).to eq('engaged and optimistic')
118
+ expect(prose.emotion_label_for(0.3)).to eq('calm and steady')
119
+ expect(prose.emotion_label_for(0.0)).to eq('emotionally neutral')
120
+ expect(prose.emotion_label_for(-0.3)).to eq('slightly uneasy')
121
+ expect(prose.emotion_label_for(-0.8)).to eq('distressed')
122
+ end
123
+ end
124
+
125
+ describe '.health_label_for' do
126
+ it 'returns correct labels for health ranges' do
127
+ expect(prose.health_label_for(0.95)).to eq('operating at full capacity')
128
+ expect(prose.health_label_for(0.8)).to eq('functioning well overall')
129
+ expect(prose.health_label_for(0.6)).to eq('showing some strain')
130
+ expect(prose.health_label_for(0.4)).to eq('experiencing significant cognitive difficulty')
131
+ expect(prose.health_label_for(0.2)).to eq('in cognitive distress')
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Language::Narrator::Helpers::Synthesizer do
4
+ let(:synth) { described_class }
5
+
6
+ describe '.narrate' do
7
+ it 'produces a narrative entry with all sections' do
8
+ result = synth.narrate(
9
+ tick_results: {
10
+ emotional_evaluation: { valence: 0.5, arousal: 0.8 },
11
+ sensory_processing: { spotlight: 2, peripheral: 4 }
12
+ },
13
+ cognitive_state: {
14
+ curiosity: { intensity: 0.6, active_count: 3, top_question: 'Why?' }
15
+ }
16
+ )
17
+
18
+ expect(result[:narrative]).to be_a(String)
19
+ expect(result[:narrative].length).to be > 10
20
+ expect(result[:mood]).to be_a(Symbol)
21
+ expect(result[:timestamp]).to be_a(Time)
22
+ expect(result[:sections]).to have_key(:attention)
23
+ expect(result[:sections]).to have_key(:emotion)
24
+ expect(result[:sections]).to have_key(:curiosity)
25
+ expect(result[:sections]).to have_key(:prediction)
26
+ expect(result[:sections]).to have_key(:memory)
27
+ expect(result[:sections]).to have_key(:reflection)
28
+ end
29
+
30
+ it 'handles empty inputs gracefully' do
31
+ result = synth.narrate(tick_results: {}, cognitive_state: {})
32
+ expect(result[:narrative]).to be_a(String)
33
+ expect(result[:mood]).to eq(:neutral)
34
+ end
35
+ end
36
+
37
+ describe '.infer_mood' do
38
+ it 'returns :energized for positive valence + high arousal' do
39
+ expect(synth.infer_mood({ emotional_evaluation: { valence: 0.5, arousal: 0.7 } }, {})).to eq(:energized)
40
+ end
41
+
42
+ it 'returns :content for positive valence + low arousal' do
43
+ expect(synth.infer_mood({ emotional_evaluation: { valence: 0.5, arousal: 0.3 } }, {})).to eq(:content)
44
+ end
45
+
46
+ it 'returns :anxious for negative valence + high arousal' do
47
+ expect(synth.infer_mood({ emotional_evaluation: { valence: -0.5, arousal: 0.7 } }, {})).to eq(:anxious)
48
+ end
49
+
50
+ it 'returns :subdued for negative valence + low arousal' do
51
+ expect(synth.infer_mood({ emotional_evaluation: { valence: -0.5, arousal: 0.3 } }, {})).to eq(:subdued)
52
+ end
53
+
54
+ it 'returns :alert for neutral valence + very high arousal' do
55
+ expect(synth.infer_mood({ emotional_evaluation: { valence: 0.0, arousal: 0.9 } }, {})).to eq(:alert)
56
+ end
57
+
58
+ it 'returns :dormant for neutral valence + very low arousal' do
59
+ expect(synth.infer_mood({ emotional_evaluation: { valence: 0.0, arousal: 0.1 } }, {})).to eq(:dormant)
60
+ end
61
+
62
+ it 'returns :neutral for moderate values' do
63
+ expect(synth.infer_mood({ emotional_evaluation: { valence: 0.0, arousal: 0.5 } }, {})).to eq(:neutral)
64
+ end
65
+ end
66
+
67
+ describe '.classify_mood' do
68
+ it 'classifies all seven moods' do
69
+ expect(synth.classify_mood(0.5, 0.8)).to eq(:energized)
70
+ expect(synth.classify_mood(0.5, 0.3)).to eq(:content)
71
+ expect(synth.classify_mood(-0.5, 0.8)).to eq(:anxious)
72
+ expect(synth.classify_mood(-0.5, 0.3)).to eq(:subdued)
73
+ expect(synth.classify_mood(0.0, 0.9)).to eq(:alert)
74
+ expect(synth.classify_mood(0.0, 0.1)).to eq(:dormant)
75
+ expect(synth.classify_mood(0.0, 0.5)).to eq(:neutral)
76
+ end
77
+ end
78
+
79
+ describe '.extract_focused_domains' do
80
+ it 'extracts domain names from manual_focus hash' do
81
+ focus = { manual_focus: { terraform: {}, vault: {} } }
82
+ expect(synth.extract_focused_domains(focus)).to eq(%w[terraform vault])
83
+ end
84
+
85
+ it 'returns empty array for nil' do
86
+ expect(synth.extract_focused_domains({})).to eq([])
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Language::Narrator::Runners::Narrator, 'LLM integration' do
4
+ let(:client) { Legion::Extensions::Agentic::Language::Narrator::Client.new }
5
+
6
+ describe '#narrate with LLM available' do
7
+ before do
8
+ response_double = double('response', content: 'I feel a deep sense of focus and possibility.')
9
+ chat_double = double('chat')
10
+ allow(chat_double).to receive(:with_instructions)
11
+ allow(chat_double).to receive(:ask).and_return(response_double)
12
+ llm_double = double('Legion::LLM', started?: true)
13
+ allow(llm_double).to receive(:chat).and_return(chat_double)
14
+ stub_const('Legion::LLM', llm_double)
15
+ end
16
+
17
+ it 'returns source: :llm when LLM narrate succeeds' do
18
+ result = client.narrate(
19
+ tick_results: { emotional_evaluation: { valence: 0.6, arousal: 0.7 } },
20
+ cognitive_state: {}
21
+ )
22
+ expect(result[:source]).to eq(:llm)
23
+ end
24
+
25
+ it 'returns the LLM narrative string' do
26
+ result = client.narrate(tick_results: {}, cognitive_state: {})
27
+ expect(result[:narrative]).to eq('I feel a deep sense of focus and possibility.')
28
+ end
29
+
30
+ it 'still includes mood and timestamp' do
31
+ result = client.narrate(tick_results: {}, cognitive_state: {})
32
+ expect(result[:mood]).to be_a(Symbol)
33
+ expect(result[:timestamp]).to be_a(Time)
34
+ end
35
+
36
+ it 'appends to journal' do
37
+ client.narrate(tick_results: {}, cognitive_state: {})
38
+ expect(client.journal.size).to eq(1)
39
+ end
40
+ end
41
+
42
+ describe '#narrate with LLM available but narrate returns nil' do
43
+ before do
44
+ chat_double = double('chat')
45
+ allow(chat_double).to receive(:with_instructions)
46
+ allow(chat_double).to receive(:ask).and_return(nil)
47
+ llm_double = double('Legion::LLM', started?: true)
48
+ allow(llm_double).to receive(:chat).and_return(chat_double)
49
+ stub_const('Legion::LLM', llm_double)
50
+ end
51
+
52
+ it 'falls back to mechanical pipeline' do
53
+ result = client.narrate(tick_results: {}, cognitive_state: {})
54
+ expect(result).not_to have_key(:source)
55
+ expect(result[:narrative]).to be_a(String)
56
+ end
57
+ end
58
+
59
+ describe '#narrate when LLM is unavailable' do
60
+ before { hide_const('Legion::LLM') }
61
+
62
+ it 'uses mechanical pipeline without source key' do
63
+ result = client.narrate(tick_results: {}, cognitive_state: {})
64
+ expect(result).not_to have_key(:source)
65
+ expect(result[:narrative]).to be_a(String)
66
+ end
67
+
68
+ it 'still generates mood and timestamp' do
69
+ result = client.narrate(tick_results: {}, cognitive_state: {})
70
+ expect(result[:mood]).to be_a(Symbol)
71
+ expect(result[:timestamp]).to be_a(Time)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Language::Narrator::Runners::Narrator do
4
+ let(:client) { Legion::Extensions::Agentic::Language::Narrator::Client.new }
5
+
6
+ describe '#narrate' do
7
+ it 'generates and stores a narrative entry' do
8
+ result = client.narrate(
9
+ tick_results: {
10
+ emotional_evaluation: { valence: 0.6, arousal: 0.7 },
11
+ sensory_processing: { spotlight: 2, peripheral: 3 }
12
+ },
13
+ cognitive_state: {
14
+ curiosity: { intensity: 0.5, active_count: 2, top_question: 'What is happening?' }
15
+ }
16
+ )
17
+
18
+ expect(result[:narrative]).to be_a(String)
19
+ expect(result[:narrative]).to include('spotlight')
20
+ expect(result[:mood]).to be_a(Symbol)
21
+ expect(result[:timestamp]).to be_a(Time)
22
+ expect(result[:sections]).to be_a(Hash)
23
+ end
24
+
25
+ it 'appends to journal' do
26
+ client.narrate(tick_results: {}, cognitive_state: {})
27
+ expect(client.journal.size).to eq(1)
28
+ end
29
+
30
+ it 'accumulates entries over multiple narrations' do
31
+ 3.times { client.narrate(tick_results: {}, cognitive_state: {}) }
32
+ expect(client.journal.size).to eq(3)
33
+ end
34
+ end
35
+
36
+ describe '#recent_entries' do
37
+ before do
38
+ 5.times { |i| client.narrate(tick_results: { emotional_evaluation: { valence: i * 0.2 } }, cognitive_state: {}) }
39
+ end
40
+
41
+ it 'returns recent entries' do
42
+ result = client.recent_entries(limit: 3)
43
+ expect(result[:entries].size).to eq(3)
44
+ expect(result[:count]).to eq(3)
45
+ expect(result[:total]).to eq(5)
46
+ end
47
+
48
+ it 'returns all when fewer than limit' do
49
+ result = client.recent_entries(limit: 20)
50
+ expect(result[:entries].size).to eq(5)
51
+ end
52
+ end
53
+
54
+ describe '#entries_since' do
55
+ it 'returns entries after timestamp' do
56
+ client.narrate(tick_results: {}, cognitive_state: {})
57
+ cutoff = Time.now.utc
58
+ sleep 0.01
59
+ client.narrate(tick_results: {}, cognitive_state: {})
60
+
61
+ result = client.entries_since(since: cutoff)
62
+ expect(result[:count]).to eq(1)
63
+ end
64
+ end
65
+
66
+ describe '#mood_history' do
67
+ before do
68
+ client.narrate(
69
+ tick_results: { emotional_evaluation: { valence: 0.5, arousal: 0.8 } },
70
+ cognitive_state: {}
71
+ )
72
+ client.narrate(
73
+ tick_results: { emotional_evaluation: { valence: -0.5, arousal: 0.8 } },
74
+ cognitive_state: {}
75
+ )
76
+ client.narrate(
77
+ tick_results: { emotional_evaluation: { valence: 0.5, arousal: 0.8 } },
78
+ cognitive_state: {}
79
+ )
80
+ end
81
+
82
+ it 'returns all mood history without filter' do
83
+ result = client.mood_history
84
+ expect(result[:count]).to eq(3)
85
+ end
86
+
87
+ it 'filters by specific mood' do
88
+ result = client.mood_history(mood: :energized)
89
+ result[:entries].each { |e| expect(e[:mood]).to eq(:energized) }
90
+ end
91
+ end
92
+
93
+ describe '#current_narrative' do
94
+ it 'returns the most recent narrative' do
95
+ client.narrate(tick_results: { emotional_evaluation: { valence: 0.3 } }, cognitive_state: {})
96
+ result = client.current_narrative
97
+ expect(result[:narrative]).to be_a(String)
98
+ expect(result[:mood]).to be_a(Symbol)
99
+ expect(result[:age_seconds]).to be >= 0
100
+ end
101
+
102
+ it 'returns default when empty' do
103
+ result = client.current_narrative
104
+ expect(result[:narrative]).to include('No cognitive activity')
105
+ expect(result[:mood]).to eq(:dormant)
106
+ end
107
+ end
108
+
109
+ describe '#narrator_stats' do
110
+ it 'returns stats for empty journal' do
111
+ stats = client.narrator_stats
112
+ expect(stats[:journal_size]).to eq(0)
113
+ expect(stats[:capacity]).to eq(Legion::Extensions::Agentic::Language::Narrator::Helpers::Constants::MAX_JOURNAL_SIZE)
114
+ end
115
+
116
+ it 'tracks mood distribution' do
117
+ 3.times do
118
+ client.narrate(tick_results: { emotional_evaluation: { valence: 0.5, arousal: 0.8 } }, cognitive_state: {})
119
+ end
120
+ stats = client.narrator_stats
121
+ expect(stats[:journal_size]).to eq(3)
122
+ expect(stats[:dominant_mood]).to eq(:energized)
123
+ expect(stats[:mood_counts][:energized]).to eq(3)
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/agentic/language/pragmatic_inference/client'
4
+
5
+ RSpec.describe Legion::Extensions::Agentic::Language::PragmaticInference::Client do
6
+ it 'responds to all runner methods' do
7
+ client = described_class.new
8
+ expect(client).to respond_to(:analyze_utterance)
9
+ expect(client).to respond_to(:detect_maxim_violations)
10
+ expect(client).to respond_to(:generate_pragmatic_implicature)
11
+ expect(client).to respond_to(:speaker_pragmatic_profile)
12
+ expect(client).to respond_to(:utterances_by_speech_act)
13
+ expect(client).to respond_to(:utterances_by_speaker)
14
+ expect(client).to respond_to(:most_violated_maxim)
15
+ expect(client).to respond_to(:overall_cooperative_compliance)
16
+ expect(client).to respond_to(:update_pragmatic_inference)
17
+ expect(client).to respond_to(:pragmatic_inference_stats)
18
+ end
19
+ end