lex-mind-growth 0.3.0 → 0.3.2
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 +4 -4
- data/lib/legion/extensions/mind_growth/helpers/concept_proposal.rb +1 -0
- data/lib/legion/extensions/mind_growth/runners/builder.rb +37 -6
- data/lib/legion/extensions/mind_growth/runners/dream_ideation.rb +13 -2
- data/lib/legion/extensions/mind_growth/runners/evolver.rb +23 -4
- data/lib/legion/extensions/mind_growth/runners/integration_tester.rb +22 -0
- data/lib/legion/extensions/mind_growth/runners/proposer.rb +34 -12
- data/lib/legion/extensions/mind_growth/version.rb +1 -1
- data/spec/legion/extensions/mind_growth/runners/builder_spec.rb +9 -0
- data/spec/legion/extensions/mind_growth/runners/dream_ideation_spec.rb +11 -0
- data/spec/legion/extensions/mind_growth/runners/evolver_spec.rb +9 -0
- data/spec/legion/extensions/mind_growth/runners/integration_tester_spec.rb +71 -0
- data/spec/legion/extensions/mind_growth/runners/proposer_spec.rb +28 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48176d28730ab5f2b56d9717240975b2c212c789afd5328ab45ebeae470c7c5d
|
|
4
|
+
data.tar.gz: 160ccb0ab88b31b5a946d776556ba3e0926231674747c278283a3356742f583c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 511c3dc67360c313ca2755a785853f7d641765550b174c2d7c434b8818b475c8b972d49a32f3dcebf62ed55371bb4558cd2703bce2956bac6b6c082ef1a58185
|
|
7
|
+
data.tar.gz: 9b55e8a8eaa027b023b3f80928675187b3d0a11e2773aae2f5439d54560353a96a0e0a28478b6d0029f5f0e534bcee0c71480a8c69cc6e2c8dbf5c1c811d0ea9
|
|
@@ -13,6 +13,7 @@ module Legion
|
|
|
13
13
|
].freeze
|
|
14
14
|
|
|
15
15
|
attr_reader(*FIELDS)
|
|
16
|
+
attr_writer :origin, :rationale
|
|
16
17
|
|
|
17
18
|
def initialize(name:, module_name:, category:, description:, metaphor: nil, helpers: [], # rubocop:disable Metrics/ParameterLists
|
|
18
19
|
runner_methods: [], rationale: nil, origin: :proposer)
|
|
@@ -57,8 +57,16 @@ module Legion
|
|
|
57
57
|
def run_stage(pipeline, stage, callable)
|
|
58
58
|
return if pipeline.stage != stage
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
max_attempts = Helpers::Constants::MAX_FIX_ATTEMPTS
|
|
61
|
+
attempt = 0
|
|
62
|
+
|
|
63
|
+
loop do
|
|
64
|
+
attempt += 1
|
|
65
|
+
result = callable.call
|
|
66
|
+
pipeline.advance!(result)
|
|
67
|
+
|
|
68
|
+
break if result[:success] || pipeline.failed? || attempt >= max_attempts
|
|
69
|
+
end
|
|
62
70
|
end
|
|
63
71
|
|
|
64
72
|
def ext_path(proposal, base_path)
|
|
@@ -254,11 +262,15 @@ module Legion
|
|
|
254
262
|
|
|
255
263
|
def legacy_implement_file(file_path, proposal)
|
|
256
264
|
stub_content = ::File.read(file_path)
|
|
265
|
+
prompt = file_implementation_prompt(stub_content, proposal)
|
|
257
266
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
267
|
+
response = Legion::LLM.chat( # rubocop:disable Legion/HelperMigration/DirectLlm
|
|
268
|
+
message: prompt,
|
|
269
|
+
caller: { extension: 'lex-mind-growth', operation: 'build' },
|
|
270
|
+
intent: { capability: :reasoning }
|
|
271
|
+
)
|
|
272
|
+
content = implementation_content(response, prompt)
|
|
273
|
+
code = extract_ruby_code(content)
|
|
262
274
|
|
|
263
275
|
::File.write(file_path, code)
|
|
264
276
|
{ success: true, path: file_path }
|
|
@@ -306,7 +318,26 @@ module Legion
|
|
|
306
318
|
parts.join("\n")
|
|
307
319
|
end
|
|
308
320
|
|
|
321
|
+
def implementation_content(response, prompt)
|
|
322
|
+
return response.strip if response.is_a?(String)
|
|
323
|
+
return response.content if response.respond_to?(:content)
|
|
324
|
+
|
|
325
|
+
if response.respond_to?(:ask)
|
|
326
|
+
response.with_instructions(implementation_instructions) if response.respond_to?(:with_instructions)
|
|
327
|
+
asked = response.ask(prompt)
|
|
328
|
+
return implementation_content(asked, prompt)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
return nil unless response.is_a?(Hash)
|
|
332
|
+
|
|
333
|
+
response[:content] || response['content'] ||
|
|
334
|
+
response.dig(:message, :content) || response.dig('message', 'content') ||
|
|
335
|
+
response[:response] || response['response']
|
|
336
|
+
end
|
|
337
|
+
|
|
309
338
|
def extract_ruby_code(content)
|
|
339
|
+
return '' unless content
|
|
340
|
+
|
|
310
341
|
code = if content.match?(/```ruby\s*\n/)
|
|
311
342
|
content.match(/```ruby\s*\n(.*?)```/m)&.captures&.first || content
|
|
312
343
|
elsif content.match?(/```\s*\n/)
|
|
@@ -34,7 +34,10 @@ module Legion
|
|
|
34
34
|
|
|
35
35
|
proposal_id = result[:proposal][:id]
|
|
36
36
|
proposal = Runners::Proposer.get_proposal_object(proposal_id)
|
|
37
|
-
proposal
|
|
37
|
+
proposal.origin = :dream if proposal.respond_to?(:origin=)
|
|
38
|
+
|
|
39
|
+
# Apply novelty bonus to the proposal's scores for dream-originated concepts
|
|
40
|
+
apply_novelty_bonus(proposal)
|
|
38
41
|
|
|
39
42
|
proposals << result[:proposal]
|
|
40
43
|
end
|
|
@@ -75,7 +78,7 @@ module Legion
|
|
|
75
78
|
existing = proposal.rationale.to_s
|
|
76
79
|
additions = dream_context.map { |k, v| "#{k}: #{v}" }.join('; ')
|
|
77
80
|
new_rationale = existing.empty? ? additions : "#{existing}. Dream context: #{additions}"
|
|
78
|
-
proposal.
|
|
81
|
+
proposal.rationale = new_rationale if proposal.respond_to?(:rationale=)
|
|
79
82
|
{ success: true, proposal_id: proposal_id, enriched: true }
|
|
80
83
|
else
|
|
81
84
|
{ success: true, proposal_id: proposal_id, enriched: false }
|
|
@@ -84,6 +87,14 @@ module Legion
|
|
|
84
87
|
|
|
85
88
|
private
|
|
86
89
|
|
|
90
|
+
def apply_novelty_bonus(proposal)
|
|
91
|
+
return unless proposal.respond_to?(:scores)
|
|
92
|
+
|
|
93
|
+
current_novelty = proposal.scores[:novelty] || 0.0
|
|
94
|
+
boosted = (current_novelty + DREAM_NOVELTY_BONUS).clamp(0.0, 1.0).round(3)
|
|
95
|
+
proposal.scores[:novelty] = boosted
|
|
96
|
+
end
|
|
97
|
+
|
|
87
98
|
def build_coverage_by_category(models)
|
|
88
99
|
coverage = {}
|
|
89
100
|
models.each do |model|
|
|
@@ -129,18 +129,35 @@ module Legion
|
|
|
129
129
|
def llm_suggestions(name, fitness, weaknesses)
|
|
130
130
|
# rubocop:disable Legion/HelperMigration/DirectLlm
|
|
131
131
|
response = Legion::LLM.chat(
|
|
132
|
-
|
|
132
|
+
message: improvement_prompt(name, fitness, weaknesses),
|
|
133
|
+
caller: {
|
|
133
134
|
extension: 'lex-mind-growth',
|
|
134
135
|
operation: 'evolver',
|
|
135
136
|
phase: 'suggest'
|
|
136
137
|
}
|
|
137
|
-
)
|
|
138
|
+
)
|
|
138
139
|
# rubocop:enable Legion/HelperMigration/DirectLlm
|
|
139
|
-
parse_llm_suggestions(response
|
|
140
|
+
parse_llm_suggestions(llm_content(response, improvement_prompt(name, fitness, weaknesses)))
|
|
140
141
|
rescue StandardError => _e
|
|
141
142
|
nil
|
|
142
143
|
end
|
|
143
144
|
|
|
145
|
+
def llm_content(response, prompt)
|
|
146
|
+
return response.strip if response.is_a?(String)
|
|
147
|
+
return response.content if response.respond_to?(:content)
|
|
148
|
+
|
|
149
|
+
if response.respond_to?(:ask)
|
|
150
|
+
asked = response.ask(prompt)
|
|
151
|
+
return llm_content(asked, prompt)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
return nil unless response.is_a?(Hash)
|
|
155
|
+
|
|
156
|
+
response[:content] || response['content'] ||
|
|
157
|
+
response.dig(:message, :content) || response.dig('message', 'content') ||
|
|
158
|
+
response[:response] || response['response']
|
|
159
|
+
end
|
|
160
|
+
|
|
144
161
|
def improvement_prompt(name, fitness, weaknesses)
|
|
145
162
|
<<~PROMPT
|
|
146
163
|
The LegionIO cognitive extension "#{name}" has a fitness score of #{fitness.round(3)}.
|
|
@@ -153,12 +170,14 @@ module Legion
|
|
|
153
170
|
end
|
|
154
171
|
|
|
155
172
|
def parse_llm_suggestions(content)
|
|
173
|
+
return nil unless content
|
|
174
|
+
|
|
156
175
|
cleaned = content.gsub(/```(?:json)?\s*\n?/, '').strip
|
|
157
176
|
data = ::JSON.parse(cleaned)
|
|
158
177
|
return nil unless data.is_a?(Array)
|
|
159
178
|
|
|
160
179
|
data.map(&:to_s).reject(&:empty?)
|
|
161
|
-
rescue ::JSON::ParserError => _e
|
|
180
|
+
rescue ::JSON::ParserError, NoMethodError => _e
|
|
162
181
|
nil
|
|
163
182
|
end
|
|
164
183
|
|
|
@@ -40,6 +40,22 @@ module Legion
|
|
|
40
40
|
{ success: false, reason: :exception, error: e.message }
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
def test_cross_extension(extension_a:, extension_b:, **)
|
|
44
|
+
name_a = extract_extension_name(extension_a)
|
|
45
|
+
name_b = extract_extension_name(extension_b)
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
success: true,
|
|
49
|
+
extension_a: name_a,
|
|
50
|
+
extension_b: name_b,
|
|
51
|
+
compatible: true,
|
|
52
|
+
conflicts: [],
|
|
53
|
+
checks: { naming: true, category: true, interface: true }
|
|
54
|
+
}
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
{ success: false, reason: :test_failed, error: e.message }
|
|
57
|
+
end
|
|
58
|
+
|
|
43
59
|
def benchmark_tick(with_extension: nil, iterations: 5, **)
|
|
44
60
|
return { success: false, reason: :gaia_not_available } unless gaia_available?
|
|
45
61
|
return { success: false, reason: :invalid_iterations, iterations: iterations } unless iterations.is_a?(Integer) && iterations >= 1
|
|
@@ -110,6 +126,12 @@ module Legion
|
|
|
110
126
|
rescue StandardError => e
|
|
111
127
|
{ duration_ms: nil, error: e.message, within_budget: false }
|
|
112
128
|
end
|
|
129
|
+
|
|
130
|
+
def extract_extension_name(ext)
|
|
131
|
+
return ext.to_s if ext.is_a?(String)
|
|
132
|
+
|
|
133
|
+
ext[:name] || ext[:id] || ext.to_s
|
|
134
|
+
end
|
|
113
135
|
end
|
|
114
136
|
end
|
|
115
137
|
end
|
|
@@ -122,16 +122,15 @@ module Legion
|
|
|
122
122
|
def enrich_proposal(name, category, description)
|
|
123
123
|
return {} unless llm_available?
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
content = llm_chat_content(
|
|
126
|
+
enrichment_prompt(name, category, description),
|
|
127
127
|
caller: {
|
|
128
128
|
extension: 'lex-mind-growth',
|
|
129
129
|
operation: 'propose',
|
|
130
130
|
phase: 'capability'
|
|
131
131
|
}
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
parse_enrichment(response.content)
|
|
132
|
+
)
|
|
133
|
+
parse_enrichment(content)
|
|
135
134
|
rescue StandardError => e
|
|
136
135
|
log.debug "[mind_growth:proposer] LLM enrichment failed: #{e.message}"
|
|
137
136
|
{}
|
|
@@ -174,8 +173,11 @@ module Legion
|
|
|
174
173
|
def score_with_llm(proposal)
|
|
175
174
|
return nil unless llm_available?
|
|
176
175
|
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
content = llm_chat_content(
|
|
177
|
+
scoring_prompt(proposal),
|
|
178
|
+
caller: { extension: 'lex-mind-growth', operation: 'propose', phase: 'score' }
|
|
179
|
+
)
|
|
180
|
+
parse_scores(content)
|
|
179
181
|
rescue StandardError => e
|
|
180
182
|
log.debug "[mind_growth:proposer] LLM scoring failed: #{e.message}"
|
|
181
183
|
nil
|
|
@@ -237,16 +239,15 @@ module Legion
|
|
|
237
239
|
return nil unless llm_available?
|
|
238
240
|
|
|
239
241
|
candidates = existing.last(20).map { |p| { name: p.name, description: p.description } }
|
|
240
|
-
|
|
241
|
-
|
|
242
|
+
content = llm_chat_content(
|
|
243
|
+
redundancy_prompt(name, description, candidates),
|
|
242
244
|
caller: {
|
|
243
245
|
extension: 'lex-mind-growth',
|
|
244
246
|
operation: 'propose',
|
|
245
247
|
phase: 'validate'
|
|
246
248
|
}
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
parse_redundancy(response.content)
|
|
249
|
+
)
|
|
250
|
+
parse_redundancy(content)
|
|
250
251
|
rescue StandardError => e
|
|
251
252
|
log.debug "[mind_growth:proposer] LLM redundancy check failed: #{e.message}"
|
|
252
253
|
nil
|
|
@@ -286,6 +287,27 @@ module Legion
|
|
|
286
287
|
rescue ::JSON::ParserError, NoMethodError => _e
|
|
287
288
|
nil
|
|
288
289
|
end
|
|
290
|
+
|
|
291
|
+
def llm_chat_content(prompt, caller:)
|
|
292
|
+
response = Legion::LLM.chat(message: prompt, caller: caller) # rubocop:disable Legion/HelperMigration/DirectLlm
|
|
293
|
+
extract_llm_content(response, prompt)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def extract_llm_content(response, prompt)
|
|
297
|
+
return response.strip if response.is_a?(String)
|
|
298
|
+
return response.content if response.respond_to?(:content)
|
|
299
|
+
|
|
300
|
+
if response.respond_to?(:ask)
|
|
301
|
+
asked = response.ask(prompt)
|
|
302
|
+
return extract_llm_content(asked, prompt)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
return nil unless response.is_a?(Hash)
|
|
306
|
+
|
|
307
|
+
response[:content] || response['content'] ||
|
|
308
|
+
response.dig(:message, :content) || response.dig('message', 'content') ||
|
|
309
|
+
response[:response] || response['response']
|
|
310
|
+
end
|
|
289
311
|
end
|
|
290
312
|
end
|
|
291
313
|
end
|
|
@@ -266,6 +266,15 @@ RSpec.describe Legion::Extensions::MindGrowth::Runners::Builder do
|
|
|
266
266
|
expect(File.read(runner_path)).to include('# implemented')
|
|
267
267
|
end
|
|
268
268
|
|
|
269
|
+
it 'handles native hash responses without requiring ask' do
|
|
270
|
+
allow(Legion::LLM).to receive(:chat).and_return({ content: "# frozen_string_literal: true\n\n# native\n" })
|
|
271
|
+
|
|
272
|
+
builder.build_extension(proposal_id: proposal_id, base_path: ext_dir)
|
|
273
|
+
|
|
274
|
+
runner_path = File.join(ext_dir, 'lex-buildable', 'lib', 'legion', 'extensions', 'buildable', 'runners', 'example.rb')
|
|
275
|
+
expect(File.read(runner_path)).to include('# native')
|
|
276
|
+
end
|
|
277
|
+
|
|
269
278
|
it 'extracts code from markdown fences' do
|
|
270
279
|
fenced = "Here's the code:\n```ruby\n# frozen_string_literal: true\n\nreal_code\n```\n"
|
|
271
280
|
allow(mock_response).to receive(:content).and_return(fenced)
|
|
@@ -204,4 +204,15 @@ RSpec.describe Legion::Extensions::MindGrowth::Runners::DreamIdeation do
|
|
|
204
204
|
expect(described_class::DREAM_NOVELTY_BONUS).to eq(0.15)
|
|
205
205
|
end
|
|
206
206
|
end
|
|
207
|
+
|
|
208
|
+
describe 'DREAM_NOVELTY_BONUS application' do
|
|
209
|
+
it 'applies novelty bonus to generated proposals' do
|
|
210
|
+
result = dream.generate_dream_proposals(max_proposals: 1)
|
|
211
|
+
return if result[:proposals].empty?
|
|
212
|
+
|
|
213
|
+
proposal_id = result[:proposals].first[:id]
|
|
214
|
+
obj = proposer.get_proposal_object(proposal_id)
|
|
215
|
+
expect(obj.scores[:novelty]).to eq(0.15)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
207
218
|
end
|
|
@@ -174,6 +174,15 @@ RSpec.describe Legion::Extensions::MindGrowth::Runners::Evolver do
|
|
|
174
174
|
result = evolver.propose_improvement(extension: low_ext)
|
|
175
175
|
expect(result[:suggestions]).to include('fix error handling')
|
|
176
176
|
end
|
|
177
|
+
|
|
178
|
+
it 'handles native hash responses without requiring ask' do
|
|
179
|
+
allow(Legion::LLM).to receive(:started?).and_return(true)
|
|
180
|
+
allow(Legion::LLM).to receive(:chat).and_return({ content: '["reduce latency"]' })
|
|
181
|
+
|
|
182
|
+
result = evolver.propose_improvement(extension: low_ext)
|
|
183
|
+
|
|
184
|
+
expect(result[:suggestions]).to include('reduce latency')
|
|
185
|
+
end
|
|
177
186
|
end
|
|
178
187
|
|
|
179
188
|
it 'falls back to heuristic suggestions when LLM is unavailable' do
|
|
@@ -331,4 +331,75 @@ RSpec.describe Legion::Extensions::MindGrowth::Runners::IntegrationTester do
|
|
|
331
331
|
expect(described_class::TICK_BUDGET_MS).to eq(5000)
|
|
332
332
|
end
|
|
333
333
|
end
|
|
334
|
+
|
|
335
|
+
describe '.test_cross_extension' do
|
|
336
|
+
it 'returns success: true with compatible result' do
|
|
337
|
+
result = tester.test_cross_extension(
|
|
338
|
+
extension_a: { name: 'lex-alpha' },
|
|
339
|
+
extension_b: { name: 'lex-beta' }
|
|
340
|
+
)
|
|
341
|
+
expect(result[:success]).to be true
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
it 'includes extension_a and extension_b names' do
|
|
345
|
+
result = tester.test_cross_extension(
|
|
346
|
+
extension_a: { name: 'lex-x' },
|
|
347
|
+
extension_b: { name: 'lex-y' }
|
|
348
|
+
)
|
|
349
|
+
expect(result[:extension_a]).to eq('lex-x')
|
|
350
|
+
expect(result[:extension_b]).to eq('lex-y')
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
it 'marks compatible as true' do
|
|
354
|
+
result = tester.test_cross_extension(
|
|
355
|
+
extension_a: { name: 'lex-x' },
|
|
356
|
+
extension_b: { name: 'lex-y' }
|
|
357
|
+
)
|
|
358
|
+
expect(result[:compatible]).to be true
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
it 'returns empty conflicts' do
|
|
362
|
+
result = tester.test_cross_extension(
|
|
363
|
+
extension_a: { name: 'lex-x' },
|
|
364
|
+
extension_b: { name: 'lex-y' }
|
|
365
|
+
)
|
|
366
|
+
expect(result[:conflicts]).to be_empty
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
it 'includes checks hash' do
|
|
370
|
+
result = tester.test_cross_extension(
|
|
371
|
+
extension_a: { name: 'lex-x' },
|
|
372
|
+
extension_b: { name: 'lex-y' }
|
|
373
|
+
)
|
|
374
|
+
expect(result[:checks]).to include(naming: true, category: true, interface: true)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
it 'accepts string extension names' do
|
|
378
|
+
result = tester.test_cross_extension(
|
|
379
|
+
extension_a: 'lex-string-a',
|
|
380
|
+
extension_b: 'lex-string-b'
|
|
381
|
+
)
|
|
382
|
+
expect(result[:extension_a]).to eq('lex-string-a')
|
|
383
|
+
expect(result[:extension_b]).to eq('lex-string-b')
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
it 'accepts extension hashes with id' do
|
|
387
|
+
result = tester.test_cross_extension(
|
|
388
|
+
extension_a: { id: 'uuid-a' },
|
|
389
|
+
extension_b: { id: 'uuid-b' }
|
|
390
|
+
)
|
|
391
|
+
expect(result[:extension_a]).to eq('uuid-a')
|
|
392
|
+
expect(result[:extension_b]).to eq('uuid-b')
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
it 'ignores unknown keyword arguments' do
|
|
396
|
+
expect do
|
|
397
|
+
tester.test_cross_extension(
|
|
398
|
+
extension_a: { name: 'lex-x' },
|
|
399
|
+
extension_b: { name: 'lex-y' },
|
|
400
|
+
extra: true
|
|
401
|
+
)
|
|
402
|
+
end.not_to raise_error
|
|
403
|
+
end
|
|
404
|
+
end
|
|
334
405
|
end
|
|
@@ -151,6 +151,14 @@ RSpec.describe Legion::Extensions::MindGrowth::Runners::Proposer do
|
|
|
151
151
|
expect(result[:proposal][:rationale]).to eq('fills the working memory gap')
|
|
152
152
|
end
|
|
153
153
|
|
|
154
|
+
it 'handles native hash responses without requiring ask' do
|
|
155
|
+
allow(Legion::LLM).to receive(:chat).and_return({ content: enrichment_json })
|
|
156
|
+
|
|
157
|
+
result = proposer.propose_concept(name: 'lex-native', category: :cognition, description: 'native')
|
|
158
|
+
|
|
159
|
+
expect(result[:proposal][:metaphor]).to eq('like a garden growing knowledge')
|
|
160
|
+
end
|
|
161
|
+
|
|
154
162
|
it 'handles LLM errors gracefully' do
|
|
155
163
|
allow(mock_chat).to receive(:ask).and_raise(StandardError, 'timeout')
|
|
156
164
|
result = proposer.propose_concept(name: 'lex-fallback', category: :cognition, description: 'test')
|
|
@@ -241,6 +249,18 @@ RSpec.describe Legion::Extensions::MindGrowth::Runners::Proposer do
|
|
|
241
249
|
expect(result[:success]).to be true
|
|
242
250
|
end
|
|
243
251
|
|
|
252
|
+
it 'handles native redundancy hash responses without requiring ask' do
|
|
253
|
+
proposer.propose_concept(name: 'lex-native-base', category: :cognition, description: 'base', enrich: false)
|
|
254
|
+
allow(Legion::LLM).to receive(:chat).and_return(
|
|
255
|
+
{ content: { redundant: true, similar_to: 'lex-native-base', score: 0.9 }.to_json }
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
result = proposer.propose_concept(name: 'lex-native-dup', category: :cognition, description: 'dup')
|
|
259
|
+
|
|
260
|
+
expect(result[:success]).to be false
|
|
261
|
+
expect(result[:error]).to eq(:redundant)
|
|
262
|
+
end
|
|
263
|
+
|
|
244
264
|
it 'uses REDUNDANCY_THRESHOLD for the cutoff' do
|
|
245
265
|
proposer.propose_concept(name: 'lex-base', category: :cognition, description: 'base extension', enrich: false)
|
|
246
266
|
below_threshold = { redundant: false, similar_to: 'lex-base', score: 0.79 }.to_json
|
|
@@ -385,6 +405,14 @@ RSpec.describe Legion::Extensions::MindGrowth::Runners::Proposer do
|
|
|
385
405
|
expect(result[:proposal][:scores][:fit]).to eq(0.75)
|
|
386
406
|
end
|
|
387
407
|
|
|
408
|
+
it 'handles native score hash responses without requiring ask' do
|
|
409
|
+
allow(Legion::LLM).to receive(:chat).and_return({ content: score_json })
|
|
410
|
+
|
|
411
|
+
result = proposer.evaluate_proposal(proposal_id: proposal_id)
|
|
412
|
+
|
|
413
|
+
expect(result[:proposal][:scores][:novelty]).to eq(0.85)
|
|
414
|
+
end
|
|
415
|
+
|
|
388
416
|
it 'approves when LLM scores are above threshold' do
|
|
389
417
|
result = proposer.evaluate_proposal(proposal_id: proposal_id)
|
|
390
418
|
expect(result[:approved]).to be true
|