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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +13 -0
- data/lex-agentic-language.gemspec +30 -0
- data/lib/legion/extensions/agentic/language/conceptual_blending/client.rb +25 -0
- data/lib/legion/extensions/agentic/language/conceptual_blending/helpers/blend.rb +91 -0
- data/lib/legion/extensions/agentic/language/conceptual_blending/helpers/blending_engine.rb +171 -0
- data/lib/legion/extensions/agentic/language/conceptual_blending/helpers/constants.rb +35 -0
- data/lib/legion/extensions/agentic/language/conceptual_blending/helpers/mental_space.rb +51 -0
- data/lib/legion/extensions/agentic/language/conceptual_blending/runners/conceptual_blending.rb +106 -0
- data/lib/legion/extensions/agentic/language/conceptual_blending/version.rb +13 -0
- data/lib/legion/extensions/agentic/language/conceptual_blending.rb +20 -0
- data/lib/legion/extensions/agentic/language/conceptual_metaphor/client.rb +19 -0
- data/lib/legion/extensions/agentic/language/conceptual_metaphor/helpers/constants.rb +49 -0
- data/lib/legion/extensions/agentic/language/conceptual_metaphor/helpers/metaphor.rb +109 -0
- data/lib/legion/extensions/agentic/language/conceptual_metaphor/helpers/metaphor_engine.rb +154 -0
- data/lib/legion/extensions/agentic/language/conceptual_metaphor/runners/conceptual_metaphor.rb +107 -0
- data/lib/legion/extensions/agentic/language/conceptual_metaphor/version.rb +13 -0
- data/lib/legion/extensions/agentic/language/conceptual_metaphor.rb +19 -0
- data/lib/legion/extensions/agentic/language/frame_semantics/helpers/client.rb +23 -0
- data/lib/legion/extensions/agentic/language/frame_semantics/helpers/constants.rb +32 -0
- data/lib/legion/extensions/agentic/language/frame_semantics/helpers/frame.rb +109 -0
- data/lib/legion/extensions/agentic/language/frame_semantics/helpers/frame_engine.rb +139 -0
- data/lib/legion/extensions/agentic/language/frame_semantics/helpers/frame_instance.rb +51 -0
- data/lib/legion/extensions/agentic/language/frame_semantics/runners/frame_semantics.rb +108 -0
- data/lib/legion/extensions/agentic/language/frame_semantics/version.rb +13 -0
- data/lib/legion/extensions/agentic/language/frame_semantics.rb +20 -0
- data/lib/legion/extensions/agentic/language/grammar/client.rb +29 -0
- data/lib/legion/extensions/agentic/language/grammar/helpers/constants.rb +38 -0
- data/lib/legion/extensions/agentic/language/grammar/helpers/construal.rb +68 -0
- data/lib/legion/extensions/agentic/language/grammar/helpers/construction.rb +68 -0
- data/lib/legion/extensions/agentic/language/grammar/helpers/grammar_engine.rb +119 -0
- data/lib/legion/extensions/agentic/language/grammar/runners/cognitive_grammar.rb +100 -0
- data/lib/legion/extensions/agentic/language/grammar/version.rb +13 -0
- data/lib/legion/extensions/agentic/language/grammar.rb +20 -0
- data/lib/legion/extensions/agentic/language/inner_speech/client.rb +19 -0
- data/lib/legion/extensions/agentic/language/inner_speech/helpers/constants.rb +53 -0
- data/lib/legion/extensions/agentic/language/inner_speech/helpers/inner_voice.rb +141 -0
- data/lib/legion/extensions/agentic/language/inner_speech/helpers/speech_stream.rb +128 -0
- data/lib/legion/extensions/agentic/language/inner_speech/helpers/utterance.rb +90 -0
- data/lib/legion/extensions/agentic/language/inner_speech/runners/inner_speech.rb +87 -0
- data/lib/legion/extensions/agentic/language/inner_speech/version.rb +13 -0
- data/lib/legion/extensions/agentic/language/inner_speech.rb +20 -0
- data/lib/legion/extensions/agentic/language/language/client.rb +26 -0
- data/lib/legion/extensions/agentic/language/language/helpers/constants.rb +43 -0
- data/lib/legion/extensions/agentic/language/language/helpers/lexicon.rb +63 -0
- data/lib/legion/extensions/agentic/language/language/helpers/summarizer.rb +167 -0
- data/lib/legion/extensions/agentic/language/language/runners/language.rb +134 -0
- data/lib/legion/extensions/agentic/language/language/version.rb +13 -0
- data/lib/legion/extensions/agentic/language/language.rb +19 -0
- data/lib/legion/extensions/agentic/language/narrative_reasoning/client.rb +28 -0
- data/lib/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative.rb +123 -0
- data/lib/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_engine.rb +122 -0
- data/lib/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_event.rb +41 -0
- data/lib/legion/extensions/agentic/language/narrative_reasoning/runners/narrative_reasoning.rb +122 -0
- data/lib/legion/extensions/agentic/language/narrative_reasoning/version.rb +13 -0
- data/lib/legion/extensions/agentic/language/narrative_reasoning.rb +18 -0
- data/lib/legion/extensions/agentic/language/narrator/client.rb +27 -0
- data/lib/legion/extensions/agentic/language/narrator/helpers/constants.rb +69 -0
- data/lib/legion/extensions/agentic/language/narrator/helpers/journal.rb +68 -0
- data/lib/legion/extensions/agentic/language/narrator/helpers/llm_enhancer.rb +105 -0
- data/lib/legion/extensions/agentic/language/narrator/helpers/prose.rb +122 -0
- data/lib/legion/extensions/agentic/language/narrator/helpers/synthesizer.rb +138 -0
- data/lib/legion/extensions/agentic/language/narrator/runners/narrator.rb +196 -0
- data/lib/legion/extensions/agentic/language/narrator/version.rb +13 -0
- data/lib/legion/extensions/agentic/language/narrator.rb +21 -0
- data/lib/legion/extensions/agentic/language/pragmatic_inference/client.rb +28 -0
- data/lib/legion/extensions/agentic/language/pragmatic_inference/helpers/constants.rb +52 -0
- data/lib/legion/extensions/agentic/language/pragmatic_inference/helpers/pragmatic_engine.rb +164 -0
- data/lib/legion/extensions/agentic/language/pragmatic_inference/helpers/utterance.rb +84 -0
- data/lib/legion/extensions/agentic/language/pragmatic_inference/runners/pragmatic_inference.rb +136 -0
- data/lib/legion/extensions/agentic/language/pragmatic_inference/version.rb +13 -0
- data/lib/legion/extensions/agentic/language/pragmatic_inference.rb +18 -0
- data/lib/legion/extensions/agentic/language/version.rb +11 -0
- data/lib/legion/extensions/agentic/language.rb +28 -0
- data/spec/legion/extensions/agentic/language/conceptual_blending/client_spec.rb +78 -0
- data/spec/legion/extensions/agentic/language/conceptual_blending/helpers/blend_spec.rb +141 -0
- data/spec/legion/extensions/agentic/language/conceptual_blending/helpers/blending_engine_spec.rb +211 -0
- data/spec/legion/extensions/agentic/language/conceptual_blending/helpers/mental_space_spec.rb +85 -0
- data/spec/legion/extensions/agentic/language/conceptual_blending/runners/conceptual_blending_spec.rb +162 -0
- data/spec/legion/extensions/agentic/language/conceptual_metaphor/client_spec.rb +29 -0
- data/spec/legion/extensions/agentic/language/conceptual_metaphor/helpers/metaphor_engine_spec.rb +166 -0
- data/spec/legion/extensions/agentic/language/conceptual_metaphor/helpers/metaphor_spec.rb +133 -0
- data/spec/legion/extensions/agentic/language/conceptual_metaphor/runners/conceptual_metaphor_spec.rb +133 -0
- data/spec/legion/extensions/agentic/language/frame_semantics/helpers/frame_engine_spec.rb +227 -0
- data/spec/legion/extensions/agentic/language/frame_semantics/helpers/frame_instance_spec.rb +83 -0
- data/spec/legion/extensions/agentic/language/frame_semantics/helpers/frame_spec.rb +213 -0
- data/spec/legion/extensions/agentic/language/frame_semantics/runners/frame_semantics_spec.rb +155 -0
- data/spec/legion/extensions/agentic/language/grammar/client_spec.rb +121 -0
- data/spec/legion/extensions/agentic/language/grammar/cognitive_grammar_spec.rb +18 -0
- data/spec/legion/extensions/agentic/language/grammar/helpers/constants_spec.rb +67 -0
- data/spec/legion/extensions/agentic/language/grammar/helpers/construal_spec.rb +124 -0
- data/spec/legion/extensions/agentic/language/grammar/helpers/construction_spec.rb +155 -0
- data/spec/legion/extensions/agentic/language/grammar/helpers/grammar_engine_spec.rb +206 -0
- data/spec/legion/extensions/agentic/language/grammar/runners/cognitive_grammar_spec.rb +189 -0
- data/spec/legion/extensions/agentic/language/inner_speech/client_spec.rb +39 -0
- data/spec/legion/extensions/agentic/language/inner_speech/helpers/inner_voice_spec.rb +185 -0
- data/spec/legion/extensions/agentic/language/inner_speech/helpers/speech_stream_spec.rb +158 -0
- data/spec/legion/extensions/agentic/language/inner_speech/helpers/utterance_spec.rb +121 -0
- data/spec/legion/extensions/agentic/language/inner_speech/runners/inner_speech_spec.rb +102 -0
- data/spec/legion/extensions/agentic/language/language/client_spec.rb +20 -0
- data/spec/legion/extensions/agentic/language/language/helpers/constants_spec.rb +31 -0
- data/spec/legion/extensions/agentic/language/language/helpers/lexicon_spec.rb +116 -0
- data/spec/legion/extensions/agentic/language/language/helpers/summarizer_spec.rb +224 -0
- data/spec/legion/extensions/agentic/language/language/runners/language_spec.rb +169 -0
- data/spec/legion/extensions/agentic/language/narrative_reasoning/client_spec.rb +19 -0
- data/spec/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_engine_spec.rb +182 -0
- data/spec/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_event_spec.rb +61 -0
- data/spec/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_spec.rb +168 -0
- data/spec/legion/extensions/agentic/language/narrative_reasoning/runners/narrative_reasoning_spec.rb +174 -0
- data/spec/legion/extensions/agentic/language/narrator/client_spec.rb +24 -0
- data/spec/legion/extensions/agentic/language/narrator/helpers/journal_spec.rb +95 -0
- data/spec/legion/extensions/agentic/language/narrator/helpers/llm_enhancer_spec.rb +107 -0
- data/spec/legion/extensions/agentic/language/narrator/helpers/prose_spec.rb +134 -0
- data/spec/legion/extensions/agentic/language/narrator/helpers/synthesizer_spec.rb +89 -0
- data/spec/legion/extensions/agentic/language/narrator/runners/narrator_llm_spec.rb +74 -0
- data/spec/legion/extensions/agentic/language/narrator/runners/narrator_spec.rb +126 -0
- data/spec/legion/extensions/agentic/language/pragmatic_inference/client_spec.rb +19 -0
- data/spec/legion/extensions/agentic/language/pragmatic_inference/helpers/constants_spec.rb +73 -0
- data/spec/legion/extensions/agentic/language/pragmatic_inference/helpers/pragmatic_engine_spec.rb +185 -0
- data/spec/legion/extensions/agentic/language/pragmatic_inference/helpers/utterance_spec.rb +111 -0
- data/spec/legion/extensions/agentic/language/pragmatic_inference/runners/pragmatic_inference_spec.rb +231 -0
- data/spec/spec_helper.rb +33 -0
- metadata +210 -0
data/spec/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_engine_spec.rb
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Agentic::Language::NarrativeReasoning::Helpers::NarrativeEngine do
|
|
4
|
+
let(:engine) { described_class.new }
|
|
5
|
+
|
|
6
|
+
describe '#create_narrative' do
|
|
7
|
+
it 'creates a new narrative' do
|
|
8
|
+
narrative = engine.create_narrative(title: 'Test Story', domain: 'test')
|
|
9
|
+
expect(narrative).to be_a(Legion::Extensions::Agentic::Language::NarrativeReasoning::Helpers::Narrative)
|
|
10
|
+
expect(narrative.title).to eq('Test Story')
|
|
11
|
+
expect(narrative.domain).to eq('test')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'stores the narrative' do
|
|
15
|
+
narrative = engine.create_narrative(title: 'Stored')
|
|
16
|
+
expect(engine.get(narrative.id)).to eq(narrative)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'increments count' do
|
|
20
|
+
engine.create_narrative(title: 'First')
|
|
21
|
+
engine.create_narrative(title: 'Second')
|
|
22
|
+
expect(engine.count).to eq(2)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe '#add_narrative_event' do
|
|
27
|
+
let(:narrative) { engine.create_narrative(title: 'Events Test') }
|
|
28
|
+
|
|
29
|
+
it 'adds an event to the narrative' do
|
|
30
|
+
event = engine.add_narrative_event(
|
|
31
|
+
narrative_id: narrative.id,
|
|
32
|
+
content: 'Something happened',
|
|
33
|
+
event_type: :action
|
|
34
|
+
)
|
|
35
|
+
expect(event).to be_a(Legion::Extensions::Agentic::Language::NarrativeReasoning::Helpers::NarrativeEvent)
|
|
36
|
+
expect(narrative.events.size).to eq(1)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'returns nil for unknown narrative' do
|
|
40
|
+
result = engine.add_narrative_event(
|
|
41
|
+
narrative_id: 'no-such-id',
|
|
42
|
+
content: 'x',
|
|
43
|
+
event_type: :action
|
|
44
|
+
)
|
|
45
|
+
expect(result).to be_nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'returns nil for invalid event_type' do
|
|
49
|
+
result = engine.add_narrative_event(
|
|
50
|
+
narrative_id: narrative.id,
|
|
51
|
+
content: 'x',
|
|
52
|
+
event_type: :invalid_type
|
|
53
|
+
)
|
|
54
|
+
expect(result).to be_nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'tracks total_events count' do
|
|
58
|
+
engine.add_narrative_event(narrative_id: narrative.id, content: 'a', event_type: :action)
|
|
59
|
+
engine.add_narrative_event(narrative_id: narrative.id, content: 'b', event_type: :conflict)
|
|
60
|
+
expect(engine.total_events).to eq(2)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'accepts all valid event types' do
|
|
64
|
+
described_class::EVENT_TYPES.each do |type|
|
|
65
|
+
result = engine.add_narrative_event(narrative_id: narrative.id, content: 'x', event_type: type)
|
|
66
|
+
expect(result).not_to be_nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe '#add_narrative_theme' do
|
|
72
|
+
let(:narrative) { engine.create_narrative(title: 'Theme Test') }
|
|
73
|
+
|
|
74
|
+
it 'adds a theme to the narrative' do
|
|
75
|
+
engine.add_narrative_theme(narrative_id: narrative.id, theme: 'identity')
|
|
76
|
+
expect(narrative.themes).to include('identity')
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'returns nil for unknown narrative' do
|
|
80
|
+
result = engine.add_narrative_theme(narrative_id: 'unknown', theme: 'x')
|
|
81
|
+
expect(result).to be_nil
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe '#advance_narrative' do
|
|
86
|
+
let(:narrative) { engine.create_narrative(title: 'Arc Test') }
|
|
87
|
+
|
|
88
|
+
it 'advances arc stage' do
|
|
89
|
+
new_stage = engine.advance_narrative(narrative_id: narrative.id)
|
|
90
|
+
expect(new_stage).to eq(:rising_action)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'returns nil for unknown narrative' do
|
|
94
|
+
result = engine.advance_narrative(narrative_id: 'bad')
|
|
95
|
+
expect(result).to be_nil
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
describe '#trace_causal_chain' do
|
|
100
|
+
let(:narrative) { engine.create_narrative(title: 'Causal Test') }
|
|
101
|
+
|
|
102
|
+
it 'returns empty array for narrative with no causes' do
|
|
103
|
+
engine.add_narrative_event(narrative_id: narrative.id, content: 'start', event_type: :action)
|
|
104
|
+
chain = engine.trace_causal_chain(narrative_id: narrative.id)
|
|
105
|
+
expect(chain).to be_empty
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'returns causal links when events have causes' do
|
|
109
|
+
evt1 = engine.add_narrative_event(narrative_id: narrative.id, content: 'start', event_type: :action)
|
|
110
|
+
engine.add_narrative_event(
|
|
111
|
+
narrative_id: narrative.id,
|
|
112
|
+
content: 'consequence',
|
|
113
|
+
event_type: :resolution,
|
|
114
|
+
causes: [evt1.id]
|
|
115
|
+
)
|
|
116
|
+
chain = engine.trace_causal_chain(narrative_id: narrative.id)
|
|
117
|
+
expect(chain.size).to eq(1)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'returns empty array for unknown narrative' do
|
|
121
|
+
expect(engine.trace_causal_chain(narrative_id: 'nope')).to eq([])
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe '#complete_narratives' do
|
|
126
|
+
it 'returns narratives at resolution stage' do
|
|
127
|
+
n1 = engine.create_narrative(title: 'Finished')
|
|
128
|
+
5.times { n1.advance_arc! }
|
|
129
|
+
engine.create_narrative(title: 'In Progress')
|
|
130
|
+
expect(engine.complete_narratives).to include(n1)
|
|
131
|
+
expect(engine.complete_narratives.size).to eq(1)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
describe '#by_domain' do
|
|
136
|
+
it 'returns narratives matching domain' do
|
|
137
|
+
engine.create_narrative(title: 'A', domain: 'sci-fi')
|
|
138
|
+
engine.create_narrative(title: 'B', domain: 'sci-fi')
|
|
139
|
+
engine.create_narrative(title: 'C', domain: 'mystery')
|
|
140
|
+
expect(engine.by_domain(domain: 'sci-fi').size).to eq(2)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
describe '#most_coherent' do
|
|
145
|
+
it 'returns narratives sorted by coherence descending' do
|
|
146
|
+
engine.create_narrative(title: 'Low')
|
|
147
|
+
high = engine.create_narrative(title: 'High')
|
|
148
|
+
5.times { high.add_event(Legion::Extensions::Agentic::Language::NarrativeReasoning::Helpers::NarrativeEvent.new(content: 'x', event_type: :action)) }
|
|
149
|
+
result = engine.most_coherent(limit: 2)
|
|
150
|
+
expect(result.first).to eq(high)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it 'respects the limit' do
|
|
154
|
+
3.times { |i| engine.create_narrative(title: "N#{i}") }
|
|
155
|
+
expect(engine.most_coherent(limit: 2).size).to be <= 2
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
describe '#decay_all' do
|
|
160
|
+
it 'reduces coherence of all narratives' do
|
|
161
|
+
n = engine.create_narrative(title: 'Decay Test')
|
|
162
|
+
before = n.coherence
|
|
163
|
+
engine.decay_all
|
|
164
|
+
expect(n.coherence).to be < before
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
describe '#to_h' do
|
|
169
|
+
it 'includes count and narratives array' do
|
|
170
|
+
engine.create_narrative(title: 'One')
|
|
171
|
+
h = engine.to_h
|
|
172
|
+
expect(h[:count]).to eq(1)
|
|
173
|
+
expect(h[:narratives]).to be_an(Array)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
describe 'EVENT_TYPES constant' do
|
|
178
|
+
it 'includes the five required types' do
|
|
179
|
+
expect(described_class::EVENT_TYPES).to include(:action, :discovery, :conflict, :resolution, :revelation)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
data/spec/legion/extensions/agentic/language/narrative_reasoning/helpers/narrative_event_spec.rb
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Agentic::Language::NarrativeReasoning::Helpers::NarrativeEvent do
|
|
4
|
+
let(:event) do
|
|
5
|
+
described_class.new(
|
|
6
|
+
content: 'The detective discovers the clue',
|
|
7
|
+
event_type: :discovery,
|
|
8
|
+
characters: %w[detective suspect],
|
|
9
|
+
causes: [],
|
|
10
|
+
domain: 'mystery'
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '#initialize' do
|
|
15
|
+
it 'assigns a uuid id' do
|
|
16
|
+
expect(event.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'stores content' do
|
|
20
|
+
expect(event.content).to eq('The detective discovers the clue')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'stores event_type' do
|
|
24
|
+
expect(event.event_type).to eq(:discovery)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'stores characters as array' do
|
|
28
|
+
expect(event.characters).to contain_exactly('detective', 'suspect')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'stores causes' do
|
|
32
|
+
expect(event.causes).to eq([])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'stores domain' do
|
|
36
|
+
expect(event.domain).to eq('mystery')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'sets timestamp to now' do
|
|
40
|
+
expect(event.timestamp).to be_a(Time)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'accepts explicit id' do
|
|
44
|
+
custom = described_class.new(content: 'x', event_type: :action, id: 'custom-id')
|
|
45
|
+
expect(custom.id).to eq('custom-id')
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe '#to_h' do
|
|
50
|
+
it 'returns a hash with all fields' do
|
|
51
|
+
h = event.to_h
|
|
52
|
+
expect(h[:id]).to eq(event.id)
|
|
53
|
+
expect(h[:content]).to eq('The detective discovers the clue')
|
|
54
|
+
expect(h[:event_type]).to eq(:discovery)
|
|
55
|
+
expect(h[:characters]).to eq(%w[detective suspect])
|
|
56
|
+
expect(h[:causes]).to eq([])
|
|
57
|
+
expect(h[:domain]).to eq('mystery')
|
|
58
|
+
expect(h[:timestamp]).to be_a(Time)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Agentic::Language::NarrativeReasoning::Helpers::Narrative do
|
|
4
|
+
let(:narrative) { described_class.new(title: 'The Journey', domain: 'adventure') }
|
|
5
|
+
|
|
6
|
+
let(:event1) do
|
|
7
|
+
Legion::Extensions::Agentic::Language::NarrativeReasoning::Helpers::NarrativeEvent.new(
|
|
8
|
+
content: 'Hero leaves home',
|
|
9
|
+
event_type: :action,
|
|
10
|
+
characters: ['hero']
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
let(:event2) do
|
|
15
|
+
Legion::Extensions::Agentic::Language::NarrativeReasoning::Helpers::NarrativeEvent.new(
|
|
16
|
+
content: 'Hero discovers map',
|
|
17
|
+
event_type: :discovery,
|
|
18
|
+
characters: ['hero'],
|
|
19
|
+
causes: [event1.id]
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe '#initialize' do
|
|
24
|
+
it 'assigns id, title, domain' do
|
|
25
|
+
expect(narrative.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
26
|
+
expect(narrative.title).to eq('The Journey')
|
|
27
|
+
expect(narrative.domain).to eq('adventure')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'starts at beginning arc stage' do
|
|
31
|
+
expect(narrative.arc_stage).to eq(:beginning)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'starts with default coherence' do
|
|
35
|
+
expect(narrative.coherence).to eq(described_class::DEFAULT_COHERENCE)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'starts with empty events, characters, themes' do
|
|
39
|
+
expect(narrative.events).to be_empty
|
|
40
|
+
expect(narrative.characters).to be_empty
|
|
41
|
+
expect(narrative.themes).to be_empty
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '#add_event' do
|
|
46
|
+
it 'appends the event' do
|
|
47
|
+
narrative.add_event(event1)
|
|
48
|
+
expect(narrative.events).to include(event1)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'collects unique characters' do
|
|
52
|
+
narrative.add_event(event1)
|
|
53
|
+
narrative.add_event(event2)
|
|
54
|
+
expect(narrative.characters).to contain_exactly('hero')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'boosts coherence on each event' do
|
|
58
|
+
initial = narrative.coherence
|
|
59
|
+
narrative.add_event(event1)
|
|
60
|
+
expect(narrative.coherence).to be > initial
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'does not exceed coherence ceiling' do
|
|
64
|
+
15.times { narrative.add_event(event1) }
|
|
65
|
+
expect(narrative.coherence).to be <= described_class::COHERENCE_CEILING
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'updates last_updated_at' do
|
|
69
|
+
before = narrative.last_updated_at
|
|
70
|
+
sleep(0.01)
|
|
71
|
+
narrative.add_event(event1)
|
|
72
|
+
expect(narrative.last_updated_at).to be >= before
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe '#add_theme' do
|
|
77
|
+
it 'adds a theme' do
|
|
78
|
+
narrative.add_theme('redemption')
|
|
79
|
+
expect(narrative.themes).to include('redemption')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'does not add duplicate themes' do
|
|
83
|
+
narrative.add_theme('redemption')
|
|
84
|
+
narrative.add_theme('redemption')
|
|
85
|
+
expect(narrative.themes.count('redemption')).to eq(1)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe '#advance_arc!' do
|
|
90
|
+
it 'advances to next arc stage' do
|
|
91
|
+
narrative.advance_arc!
|
|
92
|
+
expect(narrative.arc_stage).to eq(:rising_action)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'does not advance past resolution' do
|
|
96
|
+
5.times { narrative.advance_arc! }
|
|
97
|
+
expect(narrative.arc_stage).to eq(:resolution)
|
|
98
|
+
narrative.advance_arc!
|
|
99
|
+
expect(narrative.arc_stage).to eq(:resolution)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
describe '#causal_chain' do
|
|
104
|
+
it 'returns empty with no causes' do
|
|
105
|
+
narrative.add_event(event1)
|
|
106
|
+
expect(narrative.causal_chain).to be_empty
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'returns cause->effect links' do
|
|
110
|
+
narrative.add_event(event1)
|
|
111
|
+
narrative.add_event(event2)
|
|
112
|
+
chain = narrative.causal_chain
|
|
113
|
+
expect(chain.size).to eq(1)
|
|
114
|
+
expect(chain.first[:cause][:id]).to eq(event1.id)
|
|
115
|
+
expect(chain.first[:effect][:id]).to eq(event2.id)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
describe '#coherence_label' do
|
|
120
|
+
it 'returns :developing for default coherence' do
|
|
121
|
+
expect(narrative.coherence_label).to eq(:developing)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'returns :compelling for high coherence' do
|
|
125
|
+
10.times { narrative.add_event(event1) }
|
|
126
|
+
expect(narrative.coherence_label).to eq(:compelling)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
describe '#complete?' do
|
|
131
|
+
it 'returns false at start' do
|
|
132
|
+
expect(narrative.complete?).to be false
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'returns true at resolution' do
|
|
136
|
+
5.times { narrative.advance_arc! }
|
|
137
|
+
expect(narrative.complete?).to be true
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
describe '#decay_coherence' do
|
|
142
|
+
it 'reduces coherence by DECAY_RATE' do
|
|
143
|
+
before = narrative.coherence
|
|
144
|
+
narrative.decay_coherence
|
|
145
|
+
expect(narrative.coherence).to be_within(0.001).of(before - described_class::DECAY_RATE)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'does not go below coherence floor' do
|
|
149
|
+
200.times { narrative.decay_coherence }
|
|
150
|
+
expect(narrative.coherence).to eq(described_class::COHERENCE_FLOOR)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
describe '#to_h' do
|
|
155
|
+
it 'includes all expected keys' do
|
|
156
|
+
h = narrative.to_h
|
|
157
|
+
expect(h).to include(:id, :title, :domain, :events, :characters, :themes,
|
|
158
|
+
:arc_stage, :coherence, :coherence_label, :complete,
|
|
159
|
+
:created_at, :last_updated_at)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
describe 'ARC_STAGES constant' do
|
|
164
|
+
it 'contains the five expected stages in order' do
|
|
165
|
+
expect(described_class::ARC_STAGES).to eq(%i[beginning rising_action climax falling_action resolution])
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
data/spec/legion/extensions/agentic/language/narrative_reasoning/runners/narrative_reasoning_spec.rb
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/agentic/language/narrative_reasoning/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Agentic::Language::NarrativeReasoning::Runners::NarrativeReasoning do
|
|
6
|
+
let(:client) { Legion::Extensions::Agentic::Language::NarrativeReasoning::Client.new }
|
|
7
|
+
|
|
8
|
+
describe '#create_narrative' do
|
|
9
|
+
it 'returns success with narrative_id and title' do
|
|
10
|
+
result = client.create_narrative(title: 'My Story', domain: 'fantasy')
|
|
11
|
+
expect(result[:success]).to be true
|
|
12
|
+
expect(result[:narrative_id]).to match(/\A[0-9a-f-]{36}\z/)
|
|
13
|
+
expect(result[:title]).to eq('My Story')
|
|
14
|
+
expect(result[:arc_stage]).to eq(:beginning)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe '#add_narrative_event' do
|
|
19
|
+
let(:narrative_id) { client.create_narrative(title: 'Event Test')[:narrative_id] }
|
|
20
|
+
|
|
21
|
+
it 'adds an event and returns event_id' do
|
|
22
|
+
result = client.add_narrative_event(
|
|
23
|
+
narrative_id: narrative_id,
|
|
24
|
+
content: 'Hero acts',
|
|
25
|
+
event_type: :action,
|
|
26
|
+
characters: ['hero']
|
|
27
|
+
)
|
|
28
|
+
expect(result[:success]).to be true
|
|
29
|
+
expect(result[:event_id]).to match(/\A[0-9a-f-]{36}\z/)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'returns error for unknown narrative' do
|
|
33
|
+
result = client.add_narrative_event(narrative_id: 'bad', content: 'x', event_type: :action)
|
|
34
|
+
expect(result[:success]).to be false
|
|
35
|
+
expect(result[:error]).to eq(:narrative_not_found)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'returns error for invalid event_type' do
|
|
39
|
+
result = client.add_narrative_event(narrative_id: narrative_id, content: 'x', event_type: :bogus)
|
|
40
|
+
expect(result[:success]).to be false
|
|
41
|
+
expect(result[:error]).to eq(:invalid_event_type)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '#add_narrative_theme' do
|
|
46
|
+
let(:narrative_id) { client.create_narrative(title: 'Theme Test')[:narrative_id] }
|
|
47
|
+
|
|
48
|
+
it 'adds a theme successfully' do
|
|
49
|
+
result = client.add_narrative_theme(narrative_id: narrative_id, theme: 'courage')
|
|
50
|
+
expect(result[:success]).to be true
|
|
51
|
+
expect(result[:theme]).to eq('courage')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'returns error for unknown narrative' do
|
|
55
|
+
result = client.add_narrative_theme(narrative_id: 'nope', theme: 'x')
|
|
56
|
+
expect(result[:success]).to be false
|
|
57
|
+
expect(result[:error]).to eq(:narrative_not_found)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
describe '#advance_narrative_arc' do
|
|
62
|
+
let(:narrative_id) { client.create_narrative(title: 'Arc Test')[:narrative_id] }
|
|
63
|
+
|
|
64
|
+
it 'advances to next arc stage' do
|
|
65
|
+
result = client.advance_narrative_arc(narrative_id: narrative_id)
|
|
66
|
+
expect(result[:success]).to be true
|
|
67
|
+
expect(result[:arc_stage]).to eq(:rising_action)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'returns error for unknown narrative' do
|
|
71
|
+
result = client.advance_narrative_arc(narrative_id: 'bad')
|
|
72
|
+
expect(result[:success]).to be false
|
|
73
|
+
expect(result[:error]).to eq(:narrative_not_found)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe '#trace_narrative_causes' do
|
|
78
|
+
let(:narrative_id) { client.create_narrative(title: 'Causal Test')[:narrative_id] }
|
|
79
|
+
|
|
80
|
+
it 'returns empty chain with no causes' do
|
|
81
|
+
client.add_narrative_event(narrative_id: narrative_id, content: 'start', event_type: :action)
|
|
82
|
+
result = client.trace_narrative_causes(narrative_id: narrative_id)
|
|
83
|
+
expect(result[:success]).to be true
|
|
84
|
+
expect(result[:chain]).to eq([])
|
|
85
|
+
expect(result[:link_count]).to eq(0)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'returns causal links' do
|
|
89
|
+
evt = client.add_narrative_event(narrative_id: narrative_id, content: 'cause', event_type: :action)
|
|
90
|
+
client.add_narrative_event(
|
|
91
|
+
narrative_id: narrative_id,
|
|
92
|
+
content: 'effect',
|
|
93
|
+
event_type: :resolution,
|
|
94
|
+
causes: [evt[:event_id]]
|
|
95
|
+
)
|
|
96
|
+
result = client.trace_narrative_causes(narrative_id: narrative_id)
|
|
97
|
+
expect(result[:link_count]).to eq(1)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'returns error for unknown narrative' do
|
|
101
|
+
result = client.trace_narrative_causes(narrative_id: 'nope')
|
|
102
|
+
expect(result[:success]).to be false
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
describe '#complete_narratives' do
|
|
107
|
+
it 'returns empty list when none are complete' do
|
|
108
|
+
client.create_narrative(title: 'Incomplete')
|
|
109
|
+
result = client.complete_narratives
|
|
110
|
+
expect(result[:success]).to be true
|
|
111
|
+
expect(result[:count]).to eq(0)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'includes narratives that reached resolution' do
|
|
115
|
+
nid = client.create_narrative(title: 'Done')[:narrative_id]
|
|
116
|
+
5.times { client.advance_narrative_arc(narrative_id: nid) }
|
|
117
|
+
result = client.complete_narratives
|
|
118
|
+
expect(result[:count]).to eq(1)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
describe '#domain_narratives' do
|
|
123
|
+
it 'filters by domain' do
|
|
124
|
+
client.create_narrative(title: 'A', domain: 'sci-fi')
|
|
125
|
+
client.create_narrative(title: 'B', domain: 'sci-fi')
|
|
126
|
+
client.create_narrative(title: 'C', domain: 'horror')
|
|
127
|
+
result = client.domain_narratives(domain: 'sci-fi')
|
|
128
|
+
expect(result[:success]).to be true
|
|
129
|
+
expect(result[:count]).to eq(2)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
describe '#most_coherent_narratives' do
|
|
134
|
+
it 'returns up to limit narratives' do
|
|
135
|
+
3.times { |i| client.create_narrative(title: "N#{i}") }
|
|
136
|
+
result = client.most_coherent_narratives(limit: 2)
|
|
137
|
+
expect(result[:success]).to be true
|
|
138
|
+
expect(result[:narratives].size).to be <= 2
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it 'clamps limit to minimum 1' do
|
|
142
|
+
client.create_narrative(title: 'Solo')
|
|
143
|
+
result = client.most_coherent_narratives(limit: 0)
|
|
144
|
+
expect(result[:narratives].size).to be >= 1
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
describe '#update_narrative_reasoning' do
|
|
149
|
+
it 'returns success with count of updated narratives' do
|
|
150
|
+
client.create_narrative(title: 'A')
|
|
151
|
+
client.create_narrative(title: 'B')
|
|
152
|
+
result = client.update_narrative_reasoning
|
|
153
|
+
expect(result[:success]).to be true
|
|
154
|
+
expect(result[:narratives_updated]).to eq(2)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
describe '#narrative_reasoning_stats' do
|
|
159
|
+
it 'returns stats hash' do
|
|
160
|
+
client.create_narrative(title: 'Stats Test')
|
|
161
|
+
result = client.narrative_reasoning_stats
|
|
162
|
+
expect(result[:success]).to be true
|
|
163
|
+
expect(result[:total_narratives]).to eq(1)
|
|
164
|
+
expect(result[:total_events]).to be_a(Integer)
|
|
165
|
+
expect(result[:complete_narratives]).to be_a(Integer)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it 'reports top_coherence_label' do
|
|
169
|
+
client.create_narrative(title: 'Some Narrative')
|
|
170
|
+
result = client.narrative_reasoning_stats
|
|
171
|
+
expect(%i[compelling coherent developing fragmented incoherent]).to include(result[:top_coherence_label])
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::Agentic::Language::Narrator::Client do
|
|
4
|
+
subject(:client) { described_class.new }
|
|
5
|
+
|
|
6
|
+
it 'initializes with a default journal' do
|
|
7
|
+
expect(client.journal).to be_a(Legion::Extensions::Agentic::Language::Narrator::Helpers::Journal)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it 'accepts an injected journal' do
|
|
11
|
+
custom = Legion::Extensions::Agentic::Language::Narrator::Helpers::Journal.new
|
|
12
|
+
client = described_class.new(journal: custom)
|
|
13
|
+
expect(client.journal).to be(custom)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'includes the Narrator runner' do
|
|
17
|
+
expect(client).to respond_to(:narrate)
|
|
18
|
+
expect(client).to respond_to(:recent_entries)
|
|
19
|
+
expect(client).to respond_to(:entries_since)
|
|
20
|
+
expect(client).to respond_to(:mood_history)
|
|
21
|
+
expect(client).to respond_to(:current_narrative)
|
|
22
|
+
expect(client).to respond_to(:narrator_stats)
|
|
23
|
+
end
|
|
24
|
+
end
|