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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3fcaa84f5b0e455b0ac70019228ebe5bffdbd1c69228aafcad9bf92d7f91bb52
4
- data.tar.gz: 4ea8b0c108d6bf0a957cd9709147e4cfaa3c78e0b297afc85e642331f9153e4a
3
+ metadata.gz: 48176d28730ab5f2b56d9717240975b2c212c789afd5328ab45ebeae470c7c5d
4
+ data.tar.gz: 160ccb0ab88b31b5a946d776556ba3e0926231674747c278283a3356742f583c
5
5
  SHA512:
6
- metadata.gz: 911dc8e13b4dfe4cb4ee0cd5dd7cd27fc2d93a529aca350cc4d02d4f3f27b9548ac8881945bb57d150ffa8cc8e169e8f62b3590e2bbab4188dbfc0046aa69eb3
7
- data.tar.gz: d4f849194085b3b398dd41e3499b686c87b323ab4950a392eaca71133aeb943fcfe68557829a5cb6be92428bf781c90ab53d49cf877f26abe5b699562c25d07e
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
- result = callable.call
61
- pipeline.advance!(result)
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
- chat = Legion::LLM.chat(caller: { extension: 'lex-mind-growth', operation: 'build' }, intent: { capability: :reasoning }) # rubocop:disable Legion/HelperMigration/DirectLlm
259
- chat.with_instructions(implementation_instructions)
260
- response = chat.ask(file_implementation_prompt(stub_content, proposal))
261
- code = extract_ruby_code(response.content)
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&.instance_variable_set(:@origin, :dream)
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.instance_variable_set(:@rationale, new_rationale)
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
- caller: {
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
- ).ask(improvement_prompt(name, fitness, weaknesses))
138
+ )
138
139
  # rubocop:enable Legion/HelperMigration/DirectLlm
139
- parse_llm_suggestions(response.content)
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
- # rubocop:disable Legion/HelperMigration/DirectLlm
126
- response = Legion::LLM.chat(
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
- ).ask(enrichment_prompt(name, category, description))
133
- # rubocop:enable Legion/HelperMigration/DirectLlm
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
- response = Legion::LLM.chat(caller: { extension: 'lex-mind-growth', operation: 'propose', phase: 'score' }).ask(scoring_prompt(proposal)) # rubocop:disable Legion/HelperMigration/DirectLlm
178
- parse_scores(response.content)
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
- # rubocop:disable Legion/HelperMigration/DirectLlm
241
- response = Legion::LLM.chat(
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
- ).ask(redundancy_prompt(name, description, candidates))
248
- # rubocop:enable Legion/HelperMigration/DirectLlm
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
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module MindGrowth
6
- VERSION = '0.3.0'
6
+ VERSION = '0.3.2'
7
7
  end
8
8
  end
9
9
  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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-mind-growth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity