lex-mind-growth 0.3.0 → 0.3.1
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/runners/builder.rb +27 -4
- data/lib/legion/extensions/mind_growth/runners/evolver.rb +23 -4
- 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/evolver_spec.rb +9 -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: d9b428be29ef5df316acb3e4b9d6a01b826168046e1133e50c293e1ecc1a1186
|
|
4
|
+
data.tar.gz: 7a38b04f5d93dea41f294cb0011a9f74511cd24bfe9687af792edc1940dcfd03
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 33078449b48cf7cae04ceabd91690ae4900f47bc89275aab55005a2f1cb28242e5ef0780ef6321166b09e7b89a79b34b34afbd2d293243807a64c366c5303323
|
|
7
|
+
data.tar.gz: 6d77136475fc7cde737949355135e60fa90891559f29f02ee71aa522e80d5be82cbbda09e41b16fc4293070eae6c9b0466acd564e1441700c326112905035ef6
|
|
@@ -254,11 +254,15 @@ module Legion
|
|
|
254
254
|
|
|
255
255
|
def legacy_implement_file(file_path, proposal)
|
|
256
256
|
stub_content = ::File.read(file_path)
|
|
257
|
+
prompt = file_implementation_prompt(stub_content, proposal)
|
|
257
258
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
259
|
+
response = Legion::LLM.chat( # rubocop:disable Legion/HelperMigration/DirectLlm
|
|
260
|
+
message: prompt,
|
|
261
|
+
caller: { extension: 'lex-mind-growth', operation: 'build' },
|
|
262
|
+
intent: { capability: :reasoning }
|
|
263
|
+
)
|
|
264
|
+
content = implementation_content(response, prompt)
|
|
265
|
+
code = extract_ruby_code(content)
|
|
262
266
|
|
|
263
267
|
::File.write(file_path, code)
|
|
264
268
|
{ success: true, path: file_path }
|
|
@@ -306,7 +310,26 @@ module Legion
|
|
|
306
310
|
parts.join("\n")
|
|
307
311
|
end
|
|
308
312
|
|
|
313
|
+
def implementation_content(response, prompt)
|
|
314
|
+
return response.strip if response.is_a?(String)
|
|
315
|
+
return response.content if response.respond_to?(:content)
|
|
316
|
+
|
|
317
|
+
if response.respond_to?(:ask)
|
|
318
|
+
response.with_instructions(implementation_instructions) if response.respond_to?(:with_instructions)
|
|
319
|
+
asked = response.ask(prompt)
|
|
320
|
+
return implementation_content(asked, prompt)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
return nil unless response.is_a?(Hash)
|
|
324
|
+
|
|
325
|
+
response[:content] || response['content'] ||
|
|
326
|
+
response.dig(:message, :content) || response.dig('message', 'content') ||
|
|
327
|
+
response[:response] || response['response']
|
|
328
|
+
end
|
|
329
|
+
|
|
309
330
|
def extract_ruby_code(content)
|
|
331
|
+
return '' unless content
|
|
332
|
+
|
|
310
333
|
code = if content.match?(/```ruby\s*\n/)
|
|
311
334
|
content.match(/```ruby\s*\n(.*?)```/m)&.captures&.first || content
|
|
312
335
|
elsif content.match?(/```\s*\n/)
|
|
@@ -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
|
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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
|