lex-creativity 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/lib/legion/extensions/creativity/client.rb +23 -0
- data/lib/legion/extensions/creativity/helpers/constants.rb +46 -0
- data/lib/legion/extensions/creativity/helpers/creative_engine.rb +147 -0
- data/lib/legion/extensions/creativity/helpers/idea.rb +102 -0
- data/lib/legion/extensions/creativity/helpers/idea_store.rb +99 -0
- data/lib/legion/extensions/creativity/runners/creativity.rb +172 -0
- data/lib/legion/extensions/creativity/version.rb +9 -0
- data/lib/legion/extensions/creativity.rb +17 -0
- metadata +66 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 96628e16854f18c4d7b6d4e9aafbaa8de631a6e1839e86cd80e85e7e073f225a
|
|
4
|
+
data.tar.gz: 3c9b54db5e8a2c1d7c9a0f4a6324bf5ceee01a39a933db03903f2c1392a2f6fc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6208469287a145df8ac37de82cb4ff73f56d62cbb8c911468043d76fb5b29acbd8cc3c499131126fbe23aa1ef48ebd97927d47f5b24f1cb6a3b0c3aae8414e31
|
|
7
|
+
data.tar.gz: ffeb91083b77ebf1e052b85f8b97dcfef4ea20a78b82e118399d93d8bf1c87349b7982e6d3b1e3db0ae5261cb0e93a7d1abf7903f97cfa5cfc763f64a0f1dd76
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/creativity/helpers/constants'
|
|
4
|
+
require 'legion/extensions/creativity/helpers/idea'
|
|
5
|
+
require 'legion/extensions/creativity/helpers/idea_store'
|
|
6
|
+
require 'legion/extensions/creativity/helpers/creative_engine'
|
|
7
|
+
require 'legion/extensions/creativity/runners/creativity'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module Creativity
|
|
12
|
+
class Client
|
|
13
|
+
include Runners::Creativity
|
|
14
|
+
|
|
15
|
+
attr_reader :creative_engine
|
|
16
|
+
|
|
17
|
+
def initialize(creative_engine: nil, **)
|
|
18
|
+
@creative_engine = creative_engine || Helpers::CreativeEngine.new
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Creativity
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
# The three modes of creative thinking (Guilford + Boden model)
|
|
9
|
+
CREATIVITY_MODES = %i[divergent convergent combinational].freeze
|
|
10
|
+
|
|
11
|
+
# Guilford's four factors of creative thinking
|
|
12
|
+
IDEA_QUALITIES = %i[fluency flexibility originality elaboration].freeze
|
|
13
|
+
|
|
14
|
+
# Weighted contribution of each Guilford factor to composite quality (sum to 1.0)
|
|
15
|
+
QUALITY_WEIGHTS = {
|
|
16
|
+
fluency: 0.20,
|
|
17
|
+
flexibility: 0.25,
|
|
18
|
+
originality: 0.35,
|
|
19
|
+
elaboration: 0.20
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# EMA alpha for tracking creative potential (slow adaptation, stable baseline)
|
|
23
|
+
CREATIVITY_ALPHA = 0.1
|
|
24
|
+
|
|
25
|
+
# Ideas below this novelty score are too conventional to register
|
|
26
|
+
NOVELTY_THRESHOLD = 0.5
|
|
27
|
+
|
|
28
|
+
# Minimum Jaccard distance between concept sets for an interesting blend
|
|
29
|
+
BLEND_DISTANCE_MIN = 0.3
|
|
30
|
+
|
|
31
|
+
# Maximum ideas held in the store at any time
|
|
32
|
+
MAX_IDEAS = 200
|
|
33
|
+
|
|
34
|
+
# Maximum active seed concepts in the buffer
|
|
35
|
+
MAX_ACTIVE_SEEDS = 10
|
|
36
|
+
|
|
37
|
+
# Minimum ticks an idea must incubate before it can emerge
|
|
38
|
+
INCUBATION_TICKS = 20
|
|
39
|
+
|
|
40
|
+
# Idea lifecycle states
|
|
41
|
+
IDEA_STATES = %i[incubating emerged evaluated adopted discarded].freeze
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Creativity
|
|
6
|
+
module Helpers
|
|
7
|
+
class CreativeEngine
|
|
8
|
+
attr_reader :creative_potential, :idea_store
|
|
9
|
+
|
|
10
|
+
def initialize(idea_store: nil)
|
|
11
|
+
@idea_store = idea_store || IdeaStore.new
|
|
12
|
+
@creative_potential = 0.0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def diverge(prompt:, count: 5)
|
|
16
|
+
count = [count.to_i, 1].max
|
|
17
|
+
seeds = extract_seeds(prompt)
|
|
18
|
+
ideas = []
|
|
19
|
+
|
|
20
|
+
count.times do |i|
|
|
21
|
+
quality = diverge_quality_scores(i, count)
|
|
22
|
+
idea = Idea.new(
|
|
23
|
+
mode: :divergent,
|
|
24
|
+
seed_concepts: seeds + [@idea_store.seed_buffer.sample].compact,
|
|
25
|
+
description: "#{prompt} — variant #{i + 1}",
|
|
26
|
+
quality_scores: quality
|
|
27
|
+
)
|
|
28
|
+
novelty = @idea_store.compute_novelty(idea)
|
|
29
|
+
idea_with_nov = Idea.new(
|
|
30
|
+
mode: :divergent,
|
|
31
|
+
seed_concepts: idea.seed_concepts,
|
|
32
|
+
description: idea.description,
|
|
33
|
+
quality_scores: quality.merge(originality: novelty)
|
|
34
|
+
)
|
|
35
|
+
@idea_store.add(idea_with_nov)
|
|
36
|
+
ideas << idea_with_nov
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
update_potential(ideas)
|
|
40
|
+
ideas
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def converge(ideas:)
|
|
44
|
+
return [] if ideas.nil? || ideas.empty?
|
|
45
|
+
|
|
46
|
+
ranked = ideas
|
|
47
|
+
.select { |i| %i[emerged evaluated].include?(i.state) }
|
|
48
|
+
.sort_by { |i| -i.composite_quality }
|
|
49
|
+
ranked.each { |i| i.evaluate!(quality_scores: i.quality_scores) if i.state == :emerged }
|
|
50
|
+
ranked
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def blend(concept_a:, concept_b:)
|
|
54
|
+
set_a = Set.new(Array(concept_a).map(&:to_sym))
|
|
55
|
+
set_b = Set.new(Array(concept_b).map(&:to_sym))
|
|
56
|
+
distance = jaccard_distance(set_a, set_b)
|
|
57
|
+
|
|
58
|
+
return too_similar_result(distance) if distance < Constants::BLEND_DISTANCE_MIN
|
|
59
|
+
|
|
60
|
+
idea = build_blended_idea(set_a, set_b, distance, concept_a, concept_b)
|
|
61
|
+
@idea_store.add(idea)
|
|
62
|
+
update_potential([idea])
|
|
63
|
+
{ status: :ok, idea: idea }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def incubate
|
|
67
|
+
@idea_store.tick
|
|
68
|
+
@idea_store.emerge_ready
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def compute_novelty(idea, existing = nil)
|
|
72
|
+
if existing
|
|
73
|
+
set_a = idea.seed_concepts.to_set
|
|
74
|
+
return 1.0 if existing.empty?
|
|
75
|
+
|
|
76
|
+
distances = existing.map { |e| jaccard_distance(set_a, e.seed_concepts.to_set) }
|
|
77
|
+
distances.sum / distances.size.to_f
|
|
78
|
+
else
|
|
79
|
+
@idea_store.compute_novelty(idea)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def too_similar_result(distance)
|
|
86
|
+
{
|
|
87
|
+
status: :too_similar,
|
|
88
|
+
message: "Concepts are too similar (distance=#{distance.round(3)}, min=#{Constants::BLEND_DISTANCE_MIN})"
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build_blended_idea(set_a, set_b, distance, concept_a, concept_b)
|
|
93
|
+
blended_seeds = (set_a | set_b).to_a
|
|
94
|
+
quality = {
|
|
95
|
+
fluency: 0.5,
|
|
96
|
+
flexibility: distance.clamp(0.0, 1.0),
|
|
97
|
+
originality: distance.clamp(0.0, 1.0),
|
|
98
|
+
elaboration: 0.4
|
|
99
|
+
}
|
|
100
|
+
draft = Idea.new(mode: :combinational, seed_concepts: blended_seeds,
|
|
101
|
+
description: "Blend of [#{concept_a}] and [#{concept_b}]",
|
|
102
|
+
quality_scores: quality)
|
|
103
|
+
novelty = @idea_store.compute_novelty(draft)
|
|
104
|
+
Idea.new(mode: :combinational, seed_concepts: blended_seeds,
|
|
105
|
+
description: draft.description,
|
|
106
|
+
quality_scores: quality.merge(originality: novelty))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def extract_seeds(prompt)
|
|
110
|
+
prompt.to_s.downcase.split(/\W+/).reject(&:empty?).map(&:to_sym).uniq.first(5)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def diverge_quality_scores(index, total)
|
|
114
|
+
spread = total > 1 ? index.to_f / (total - 1) : 0.5
|
|
115
|
+
{
|
|
116
|
+
fluency: (0.4 + (spread * 0.4)).clamp(0.0, 1.0),
|
|
117
|
+
flexibility: (0.3 + (rand * 0.5)).clamp(0.0, 1.0),
|
|
118
|
+
originality: 0.5,
|
|
119
|
+
elaboration: (0.3 + (spread * 0.3)).clamp(0.0, 1.0)
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def update_potential(ideas)
|
|
124
|
+
return if ideas.empty?
|
|
125
|
+
|
|
126
|
+
avg_quality = ideas.sum(&:composite_quality) / ideas.size.to_f
|
|
127
|
+
@creative_potential = ema(@creative_potential, avg_quality, Constants::CREATIVITY_ALPHA)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def ema(current, observed, alpha)
|
|
131
|
+
(current * (1.0 - alpha)) + (observed * alpha)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def jaccard_distance(set_a, set_b)
|
|
135
|
+
return 1.0 if set_a.empty? && set_b.empty?
|
|
136
|
+
|
|
137
|
+
intersection = (set_a & set_b).size.to_f
|
|
138
|
+
union = (set_a | set_b).size.to_f
|
|
139
|
+
return 1.0 if union.zero?
|
|
140
|
+
|
|
141
|
+
1.0 - (intersection / union)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Creativity
|
|
6
|
+
module Helpers
|
|
7
|
+
class Idea
|
|
8
|
+
attr_reader :id, :mode, :seed_concepts, :description, :novelty_score,
|
|
9
|
+
:quality_scores, :composite_quality, :state,
|
|
10
|
+
:created_at, :evaluated_at, :incubation_ticks_remaining
|
|
11
|
+
|
|
12
|
+
def initialize(mode:, seed_concepts:, description:, novelty_score: 0.0, quality_scores: {})
|
|
13
|
+
@id = generate_id
|
|
14
|
+
@mode = mode
|
|
15
|
+
@seed_concepts = Array(seed_concepts).map(&:to_sym)
|
|
16
|
+
@description = description
|
|
17
|
+
@novelty_score = novelty_score.clamp(0.0, 1.0)
|
|
18
|
+
@quality_scores = build_quality_scores(quality_scores)
|
|
19
|
+
@composite_quality = compute_composite
|
|
20
|
+
@state = :incubating
|
|
21
|
+
@created_at = Time.now.utc
|
|
22
|
+
@evaluated_at = nil
|
|
23
|
+
@incubation_ticks_remaining = Constants::INCUBATION_TICKS
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def tick_incubation
|
|
27
|
+
@incubation_ticks_remaining = [@incubation_ticks_remaining - 1, 0].max
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ready_to_emerge?
|
|
31
|
+
@state == :incubating && @incubation_ticks_remaining.zero?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def emerge!
|
|
35
|
+
return false unless ready_to_emerge?
|
|
36
|
+
|
|
37
|
+
@state = :emerged
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def evaluate!(quality_scores: {})
|
|
42
|
+
return false unless @state == :emerged
|
|
43
|
+
|
|
44
|
+
@quality_scores = build_quality_scores(quality_scores.empty? ? @quality_scores : quality_scores)
|
|
45
|
+
@composite_quality = compute_composite
|
|
46
|
+
@evaluated_at = Time.now.utc
|
|
47
|
+
@state = :evaluated
|
|
48
|
+
true
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def adopt!
|
|
52
|
+
return false unless @state == :evaluated
|
|
53
|
+
|
|
54
|
+
@state = :adopted
|
|
55
|
+
true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def discard!
|
|
59
|
+
return false if @state == :adopted
|
|
60
|
+
|
|
61
|
+
@state = :discarded
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def to_h
|
|
66
|
+
{
|
|
67
|
+
id: @id,
|
|
68
|
+
mode: @mode,
|
|
69
|
+
seed_concepts: @seed_concepts,
|
|
70
|
+
description: @description,
|
|
71
|
+
novelty_score: @novelty_score.round(4),
|
|
72
|
+
quality_scores: @quality_scores,
|
|
73
|
+
composite_quality: @composite_quality.round(4),
|
|
74
|
+
state: @state,
|
|
75
|
+
created_at: @created_at,
|
|
76
|
+
evaluated_at: @evaluated_at,
|
|
77
|
+
incubation_ticks_remaining: @incubation_ticks_remaining
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def generate_id
|
|
84
|
+
"idea_#{Time.now.utc.to_f.to_s.gsub('.', '')}_#{rand(1000)}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build_quality_scores(scores)
|
|
88
|
+
Constants::IDEA_QUALITIES.to_h do |factor|
|
|
89
|
+
[factor, (scores[factor] || 0.0).clamp(0.0, 1.0)]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def compute_composite
|
|
94
|
+
Constants::QUALITY_WEIGHTS.sum do |factor, weight|
|
|
95
|
+
(@quality_scores[factor] || 0.0) * weight
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Creativity
|
|
6
|
+
module Helpers
|
|
7
|
+
class IdeaStore
|
|
8
|
+
attr_reader :ideas, :seed_buffer, :tick_count
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@ideas = []
|
|
12
|
+
@seed_buffer = []
|
|
13
|
+
@tick_count = 0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def add(idea)
|
|
17
|
+
@ideas << idea
|
|
18
|
+
@ideas.shift while @ideas.size > Constants::MAX_IDEAS
|
|
19
|
+
idea
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def tick
|
|
23
|
+
@tick_count += 1
|
|
24
|
+
@ideas.select { |i| i.state == :incubating }.each(&:tick_incubation)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def ingest_seeds(seeds)
|
|
28
|
+
Array(seeds).each do |seed|
|
|
29
|
+
sym = seed.to_sym
|
|
30
|
+
@seed_buffer << sym unless @seed_buffer.include?(sym)
|
|
31
|
+
end
|
|
32
|
+
@seed_buffer.shift while @seed_buffer.size > Constants::MAX_ACTIVE_SEEDS
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def emerge_ready
|
|
36
|
+
emerged = []
|
|
37
|
+
@ideas.select(&:ready_to_emerge?).each do |idea|
|
|
38
|
+
idea.emerge!
|
|
39
|
+
emerged << idea
|
|
40
|
+
end
|
|
41
|
+
emerged
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def by_state(state)
|
|
45
|
+
@ideas.select { |i| i.state == state }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def best_ideas(limit: 5)
|
|
49
|
+
@ideas
|
|
50
|
+
.select { |i| %i[emerged evaluated adopted].include?(i.state) }
|
|
51
|
+
.sort_by { |i| -i.composite_quality }
|
|
52
|
+
.first(limit)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def compute_novelty(idea)
|
|
56
|
+
existing = @ideas.reject { |i| i.id == idea.id }
|
|
57
|
+
return 1.0 if existing.empty?
|
|
58
|
+
|
|
59
|
+
set_a = idea.seed_concepts.to_set
|
|
60
|
+
distances = existing.map do |other|
|
|
61
|
+
set_b = other.seed_concepts.to_set
|
|
62
|
+
jaccard_distance(set_a, set_b)
|
|
63
|
+
end
|
|
64
|
+
distances.sum / distances.size.to_f
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def active_count
|
|
68
|
+
@ideas.count { |i| %i[incubating emerged evaluated].include?(i.state) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def stats
|
|
72
|
+
{
|
|
73
|
+
total: @ideas.size,
|
|
74
|
+
incubating: by_state(:incubating).size,
|
|
75
|
+
emerged: by_state(:emerged).size,
|
|
76
|
+
evaluated: by_state(:evaluated).size,
|
|
77
|
+
adopted: by_state(:adopted).size,
|
|
78
|
+
discarded: by_state(:discarded).size,
|
|
79
|
+
seeds: @seed_buffer.size,
|
|
80
|
+
tick_count: @tick_count
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def jaccard_distance(set_a, set_b)
|
|
87
|
+
return 1.0 if set_a.empty? && set_b.empty?
|
|
88
|
+
|
|
89
|
+
intersection = (set_a & set_b).size.to_f
|
|
90
|
+
union = (set_a | set_b).size.to_f
|
|
91
|
+
return 1.0 if union.zero?
|
|
92
|
+
|
|
93
|
+
1.0 - (intersection / union)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Creativity
|
|
6
|
+
module Runners
|
|
7
|
+
module Creativity
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def creative_tick(tick_results: {}, **)
|
|
12
|
+
seeds = harvest_seeds(tick_results)
|
|
13
|
+
creative_engine.idea_store.ingest_seeds(seeds) if seeds.any?
|
|
14
|
+
|
|
15
|
+
emerged = creative_engine.incubate
|
|
16
|
+
store = creative_engine.idea_store
|
|
17
|
+
|
|
18
|
+
Legion::Logging.debug "[creativity] tick: seeds=#{seeds.size} emerged=#{emerged.size} " \
|
|
19
|
+
"active=#{store.active_count} potential=#{creative_engine.creative_potential.round(3)}"
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
emerged_count: emerged.size,
|
|
23
|
+
active_count: store.active_count,
|
|
24
|
+
seeds_ingested: seeds.size,
|
|
25
|
+
creative_potential: creative_engine.creative_potential.round(4),
|
|
26
|
+
emerged_ideas: emerged.map(&:to_h)
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def diverge(prompt:, count: 5, **)
|
|
31
|
+
ideas = creative_engine.diverge(prompt: prompt, count: count)
|
|
32
|
+
|
|
33
|
+
Legion::Logging.debug "[creativity] diverge: prompt=#{prompt.inspect} count=#{ideas.size} " \
|
|
34
|
+
"potential=#{creative_engine.creative_potential.round(3)}"
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
mode: :divergent,
|
|
38
|
+
prompt: prompt,
|
|
39
|
+
ideas: ideas.map(&:to_h),
|
|
40
|
+
count: ideas.size,
|
|
41
|
+
potential: creative_engine.creative_potential.round(4)
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def blend_concepts(concept_a:, concept_b:, **)
|
|
46
|
+
result = creative_engine.blend(concept_a: concept_a, concept_b: concept_b)
|
|
47
|
+
|
|
48
|
+
if result[:status] == :ok
|
|
49
|
+
Legion::Logging.debug "[creativity] blend: #{concept_a} + #{concept_b} -> " \
|
|
50
|
+
"novelty=#{result[:idea].novelty_score.round(3)}"
|
|
51
|
+
{
|
|
52
|
+
status: :ok,
|
|
53
|
+
mode: :combinational,
|
|
54
|
+
idea: result[:idea].to_h,
|
|
55
|
+
potential: creative_engine.creative_potential.round(4)
|
|
56
|
+
}
|
|
57
|
+
else
|
|
58
|
+
Legion::Logging.debug "[creativity] blend rejected: #{result[:message]}"
|
|
59
|
+
result
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def evaluate_ideas(**)
|
|
64
|
+
emerged = creative_engine.idea_store.by_state(:emerged)
|
|
65
|
+
ranked = creative_engine.converge(ideas: emerged)
|
|
66
|
+
|
|
67
|
+
Legion::Logging.debug "[creativity] evaluate: #{ranked.size} ideas ranked"
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
evaluated_count: ranked.size,
|
|
71
|
+
ideas: ranked.map(&:to_h),
|
|
72
|
+
best: ranked.first&.to_h
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def adopt_idea(idea_id:, **)
|
|
77
|
+
idea = creative_engine.idea_store.ideas.find { |i| i.id == idea_id }
|
|
78
|
+
|
|
79
|
+
unless idea
|
|
80
|
+
Legion::Logging.debug "[creativity] adopt: idea_id=#{idea_id} not found"
|
|
81
|
+
return { status: :not_found, idea_id: idea_id }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if idea.adopt!
|
|
85
|
+
Legion::Logging.debug "[creativity] adopt: idea_id=#{idea_id} adopted"
|
|
86
|
+
{ status: :adopted, idea: idea.to_h }
|
|
87
|
+
else
|
|
88
|
+
Legion::Logging.debug "[creativity] adopt: idea_id=#{idea_id} state=#{idea.state} not adoptable"
|
|
89
|
+
{ status: :not_adoptable, idea_id: idea_id, current_state: idea.state }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def creative_status(**)
|
|
94
|
+
store = creative_engine.idea_store
|
|
95
|
+
best = store.best_ideas(limit: 3)
|
|
96
|
+
|
|
97
|
+
Legion::Logging.debug "[creativity] status: potential=#{creative_engine.creative_potential.round(3)} " \
|
|
98
|
+
"active=#{store.active_count}"
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
creative_potential: creative_engine.creative_potential.round(4),
|
|
102
|
+
active_count: store.active_count,
|
|
103
|
+
seed_buffer: store.seed_buffer,
|
|
104
|
+
best_ideas: best.map(&:to_h),
|
|
105
|
+
stats: store.stats
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def creativity_stats(**)
|
|
110
|
+
store = creative_engine.idea_store
|
|
111
|
+
Legion::Logging.debug '[creativity] stats'
|
|
112
|
+
|
|
113
|
+
adopted = store.by_state(:adopted)
|
|
114
|
+
discarded = store.by_state(:discarded)
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
creative_potential: creative_engine.creative_potential.round(4),
|
|
118
|
+
total_ideas: store.ideas.size,
|
|
119
|
+
active_count: store.active_count,
|
|
120
|
+
adopted_count: adopted.size,
|
|
121
|
+
discarded_count: discarded.size,
|
|
122
|
+
adoption_rate: adoption_rate(adopted, discarded),
|
|
123
|
+
modes: mode_breakdown(store),
|
|
124
|
+
average_quality: average_quality(store),
|
|
125
|
+
tick_count: store.tick_count,
|
|
126
|
+
seed_buffer_size: store.seed_buffer.size
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def creative_engine
|
|
133
|
+
@creative_engine ||= Helpers::CreativeEngine.new
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def harvest_seeds(tick_results)
|
|
137
|
+
seeds = []
|
|
138
|
+
seeds += extract_key_concepts(tick_results.dig(:memory_retrieval, :domains) || [])
|
|
139
|
+
seeds += extract_key_concepts(tick_results.dig(:attention, :focus_domain) ? [tick_results.dig(:attention, :focus_domain)] : [])
|
|
140
|
+
seeds += extract_key_concepts(tick_results.dig(:prediction_engine, :active_domains) || [])
|
|
141
|
+
seeds += extract_key_concepts(tick_results.dig(:volition, :current_domain) ? [tick_results.dig(:volition, :current_domain)] : [])
|
|
142
|
+
seeds.uniq
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def extract_key_concepts(domains)
|
|
146
|
+
Array(domains).map { |d| d.to_s.split(/\W+/) }.flatten.map(&:to_sym).reject { |s| s.to_s.empty? }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def adoption_rate(adopted, discarded)
|
|
150
|
+
total = adopted.size + discarded.size
|
|
151
|
+
return 0.0 if total.zero?
|
|
152
|
+
|
|
153
|
+
(adopted.size.to_f / total).round(4)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def mode_breakdown(store)
|
|
157
|
+
Helpers::Constants::CREATIVITY_MODES.to_h do |mode|
|
|
158
|
+
[mode, store.ideas.count { |i| i.mode == mode }]
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def average_quality(store)
|
|
163
|
+
scored = store.ideas.select { |i| %i[evaluated adopted discarded].include?(i.state) }
|
|
164
|
+
return 0.0 if scored.empty?
|
|
165
|
+
|
|
166
|
+
(scored.sum(&:composite_quality) / scored.size.to_f).round(4)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/creativity/version'
|
|
4
|
+
require 'legion/extensions/creativity/helpers/constants'
|
|
5
|
+
require 'legion/extensions/creativity/helpers/idea'
|
|
6
|
+
require 'legion/extensions/creativity/helpers/idea_store'
|
|
7
|
+
require 'legion/extensions/creativity/helpers/creative_engine'
|
|
8
|
+
require 'legion/extensions/creativity/runners/creativity'
|
|
9
|
+
require 'legion/extensions/creativity/client'
|
|
10
|
+
|
|
11
|
+
module Legion
|
|
12
|
+
module Extensions
|
|
13
|
+
module Creativity
|
|
14
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined?(:Core)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-creativity
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Matthew Iverson
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: legion-gaia
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
description: Generates novel ideas via divergent, convergent, and combinational creativity
|
|
27
|
+
modes; tracks creative potential via EMA; incubates and evaluates ideas using Guilford
|
|
28
|
+
quality factors
|
|
29
|
+
email:
|
|
30
|
+
- matt@legionIO.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- lib/legion/extensions/creativity.rb
|
|
36
|
+
- lib/legion/extensions/creativity/client.rb
|
|
37
|
+
- lib/legion/extensions/creativity/helpers/constants.rb
|
|
38
|
+
- lib/legion/extensions/creativity/helpers/creative_engine.rb
|
|
39
|
+
- lib/legion/extensions/creativity/helpers/idea.rb
|
|
40
|
+
- lib/legion/extensions/creativity/helpers/idea_store.rb
|
|
41
|
+
- lib/legion/extensions/creativity/runners/creativity.rb
|
|
42
|
+
- lib/legion/extensions/creativity/version.rb
|
|
43
|
+
homepage: https://github.com/LegionIO/lex-creativity
|
|
44
|
+
licenses:
|
|
45
|
+
- MIT
|
|
46
|
+
metadata:
|
|
47
|
+
rubygems_mfa_required: 'true'
|
|
48
|
+
rdoc_options: []
|
|
49
|
+
require_paths:
|
|
50
|
+
- lib
|
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '3.4'
|
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
requirements: []
|
|
62
|
+
rubygems_version: 3.6.9
|
|
63
|
+
specification_version: 4
|
|
64
|
+
summary: Divergent thinking and conceptual blending engine for LegionIO cognitive
|
|
65
|
+
agents
|
|
66
|
+
test_files: []
|