lex-perspective-shifting 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/Gemfile +11 -0
- data/lex-perspective-shifting.gemspec +31 -0
- data/lib/legion/extensions/perspective_shifting/client.rb +25 -0
- data/lib/legion/extensions/perspective_shifting/helpers/constants.rb +63 -0
- data/lib/legion/extensions/perspective_shifting/helpers/perspective.rb +41 -0
- data/lib/legion/extensions/perspective_shifting/helpers/perspective_view.rb +53 -0
- data/lib/legion/extensions/perspective_shifting/helpers/shifting_engine.rb +162 -0
- data/lib/legion/extensions/perspective_shifting/runners/perspective_shifting.rb +163 -0
- data/lib/legion/extensions/perspective_shifting/version.rb +9 -0
- data/lib/legion/extensions/perspective_shifting.rb +16 -0
- data/spec/legion/extensions/perspective_shifting/client_spec.rb +30 -0
- data/spec/legion/extensions/perspective_shifting/helpers/constants_spec.rb +99 -0
- data/spec/legion/extensions/perspective_shifting/helpers/perspective_spec.rb +77 -0
- data/spec/legion/extensions/perspective_shifting/helpers/perspective_view_spec.rb +105 -0
- data/spec/legion/extensions/perspective_shifting/helpers/shifting_engine_spec.rb +246 -0
- data/spec/legion/extensions/perspective_shifting/runners/perspective_shifting_spec.rb +277 -0
- data/spec/spec_helper.rb +25 -0
- metadata +79 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1ec311143b14d434d25a2f54c6ca96e8cb5da4e3ceb7bed2d6a84f69b4a11e49
|
|
4
|
+
data.tar.gz: e6f8648010602650dcf9056f3cc5d825469d65394c6e1d5e84b042999562c235
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d3958db3162c613a323d06ae97fb5c67a72f22165663c6d95c8f5d9399527f87419cf50859a2d9d0489b7a4a99031aae0f8c1dc7147e03238c00093fa88489e7
|
|
7
|
+
data.tar.gz: 45e7d3d9e28241bc6db4cccd39047851af420a15a81fbc23c5732237c7fc4a94a18a83c4d8575b8a75458ed25f63115f762130c2e586e1f7668c1267279edd13
|
data/Gemfile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/perspective_shifting/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-perspective-shifting'
|
|
7
|
+
spec.version = Legion::Extensions::PerspectiveShifting::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Perspective Shifting'
|
|
12
|
+
spec.description = 'Systematic multi-viewpoint analysis engine for brain-modeled agentic AI — ' \
|
|
13
|
+
'cycles through stakeholder, ethical, temporal, and other interpretive frames ' \
|
|
14
|
+
'to generate richer situational understanding'
|
|
15
|
+
spec.homepage = 'https://github.com/LegionIO/lex-perspective-shifting'
|
|
16
|
+
spec.license = 'MIT'
|
|
17
|
+
spec.required_ruby_version = '>= 3.4'
|
|
18
|
+
|
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
20
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-perspective-shifting'
|
|
21
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-perspective-shifting'
|
|
22
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-perspective-shifting'
|
|
23
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-perspective-shifting/issues'
|
|
24
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
25
|
+
|
|
26
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
27
|
+
Dir.glob('{lib,spec}/**/*') + %w[lex-perspective-shifting.gemspec Gemfile]
|
|
28
|
+
end
|
|
29
|
+
spec.require_paths = ['lib']
|
|
30
|
+
spec.add_development_dependency 'legion-gaia'
|
|
31
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/perspective_shifting/helpers/constants'
|
|
4
|
+
require 'legion/extensions/perspective_shifting/helpers/perspective'
|
|
5
|
+
require 'legion/extensions/perspective_shifting/helpers/perspective_view'
|
|
6
|
+
require 'legion/extensions/perspective_shifting/helpers/shifting_engine'
|
|
7
|
+
require 'legion/extensions/perspective_shifting/runners/perspective_shifting'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module PerspectiveShifting
|
|
12
|
+
class Client
|
|
13
|
+
include Runners::PerspectiveShifting
|
|
14
|
+
|
|
15
|
+
def initialize(**)
|
|
16
|
+
@shifting_engine = Helpers::ShiftingEngine.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
attr_reader :shifting_engine
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module PerspectiveShifting
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
MAX_PERSPECTIVES = 50
|
|
9
|
+
MAX_SITUATIONS = 200
|
|
10
|
+
MAX_VIEWS_PER_SITUATION = 20
|
|
11
|
+
DEFAULT_EMPATHY = 0.5
|
|
12
|
+
MIN_PERSPECTIVES_FOR_SYNTHESIS = 2
|
|
13
|
+
|
|
14
|
+
PERSPECTIVE_TYPES = %i[
|
|
15
|
+
stakeholder emotional temporal cultural ethical pragmatic creative adversarial
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
PRIORITY_TYPES = %i[
|
|
19
|
+
safety efficiency fairness innovation stability growth autonomy compliance
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
EMPATHY_LABELS = {
|
|
23
|
+
(0.8..) => :deeply_empathic,
|
|
24
|
+
(0.6...0.8) => :empathic,
|
|
25
|
+
(0.4...0.6) => :moderate,
|
|
26
|
+
(0.2...0.4) => :limited,
|
|
27
|
+
(..0.2) => :detached
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
COVERAGE_LABELS = {
|
|
31
|
+
(0.8..) => :comprehensive,
|
|
32
|
+
(0.6...0.8) => :thorough,
|
|
33
|
+
(0.4...0.6) => :partial,
|
|
34
|
+
(0.2...0.4) => :narrow,
|
|
35
|
+
(..0.2) => :blind
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
AGREEMENT_LABELS = {
|
|
39
|
+
(0.8..) => :consensus,
|
|
40
|
+
(0.6...0.8) => :agreement,
|
|
41
|
+
(0.4...0.6) => :mixed,
|
|
42
|
+
(0.2...0.4) => :disagreement,
|
|
43
|
+
(..0.2) => :conflict
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
module_function
|
|
47
|
+
|
|
48
|
+
def empathy_label(value)
|
|
49
|
+
EMPATHY_LABELS.find { |range, _| range.include?(value) }&.last || :detached
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def coverage_label(value)
|
|
53
|
+
COVERAGE_LABELS.find { |range, _| range.include?(value) }&.last || :blind
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def agreement_label(value)
|
|
57
|
+
AGREEMENT_LABELS.find { |range, _| range.include?(value) }&.last || :conflict
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module PerspectiveShifting
|
|
8
|
+
module Helpers
|
|
9
|
+
class Perspective
|
|
10
|
+
attr_reader :id, :name, :perspective_type, :priorities,
|
|
11
|
+
:expertise_domains, :empathy_level, :bias_toward, :created_at
|
|
12
|
+
|
|
13
|
+
def initialize(name:, perspective_type:, priorities: [], expertise_domains: [],
|
|
14
|
+
empathy_level: Constants::DEFAULT_EMPATHY, bias_toward: nil)
|
|
15
|
+
@id = SecureRandom.uuid
|
|
16
|
+
@name = name
|
|
17
|
+
@perspective_type = perspective_type
|
|
18
|
+
@priorities = Array(priorities)
|
|
19
|
+
@expertise_domains = Array(expertise_domains)
|
|
20
|
+
@empathy_level = empathy_level.clamp(0.0, 1.0)
|
|
21
|
+
@bias_toward = bias_toward
|
|
22
|
+
@created_at = Time.now.utc
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_h
|
|
26
|
+
{
|
|
27
|
+
id: @id,
|
|
28
|
+
name: @name,
|
|
29
|
+
perspective_type: @perspective_type,
|
|
30
|
+
priorities: @priorities,
|
|
31
|
+
expertise_domains: @expertise_domains,
|
|
32
|
+
empathy_level: @empathy_level.round(10),
|
|
33
|
+
bias_toward: @bias_toward,
|
|
34
|
+
created_at: @created_at
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module PerspectiveShifting
|
|
8
|
+
module Helpers
|
|
9
|
+
class PerspectiveView
|
|
10
|
+
attr_reader :id, :situation_id, :perspective_id, :valence,
|
|
11
|
+
:concerns, :opportunities, :confidence, :created_at
|
|
12
|
+
|
|
13
|
+
def initialize(situation_id:, perspective_id:, valence: 0.0,
|
|
14
|
+
concerns: [], opportunities: [], confidence: 0.5)
|
|
15
|
+
@id = SecureRandom.uuid
|
|
16
|
+
@situation_id = situation_id
|
|
17
|
+
@perspective_id = perspective_id
|
|
18
|
+
@valence = valence.clamp(-1.0, 1.0).round(10)
|
|
19
|
+
@concerns = Array(concerns)
|
|
20
|
+
@opportunities = Array(opportunities)
|
|
21
|
+
@confidence = confidence.clamp(0.0, 1.0).round(10)
|
|
22
|
+
@created_at = Time.now.utc
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def positive?
|
|
26
|
+
@valence > 0.1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def negative?
|
|
30
|
+
@valence < -0.1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def neutral?
|
|
34
|
+
!positive? && !negative?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_h
|
|
38
|
+
{
|
|
39
|
+
id: @id,
|
|
40
|
+
situation_id: @situation_id,
|
|
41
|
+
perspective_id: @perspective_id,
|
|
42
|
+
valence: @valence,
|
|
43
|
+
concerns: @concerns,
|
|
44
|
+
opportunities: @opportunities,
|
|
45
|
+
confidence: @confidence,
|
|
46
|
+
created_at: @created_at
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module PerspectiveShifting
|
|
8
|
+
module Helpers
|
|
9
|
+
class ShiftingEngine
|
|
10
|
+
attr_reader :perspectives, :situations
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@perspectives = {}
|
|
14
|
+
@situations = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add_perspective(name:, type: :stakeholder, priorities: [], empathy: Constants::DEFAULT_EMPATHY,
|
|
18
|
+
bias: nil, expertise_domains: [])
|
|
19
|
+
return { error: :too_many_perspectives } if @perspectives.size >= Constants::MAX_PERSPECTIVES
|
|
20
|
+
return { error: :invalid_type } unless Constants::PERSPECTIVE_TYPES.include?(type)
|
|
21
|
+
|
|
22
|
+
p = Perspective.new(
|
|
23
|
+
name: name,
|
|
24
|
+
perspective_type: type,
|
|
25
|
+
priorities: priorities,
|
|
26
|
+
expertise_domains: expertise_domains,
|
|
27
|
+
empathy_level: empathy,
|
|
28
|
+
bias_toward: bias
|
|
29
|
+
)
|
|
30
|
+
@perspectives[p.id] = p
|
|
31
|
+
p
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def add_situation(content:)
|
|
35
|
+
return { error: :too_many_situations } if @situations.size >= Constants::MAX_SITUATIONS
|
|
36
|
+
|
|
37
|
+
id = SecureRandom.uuid
|
|
38
|
+
@situations[id] = { id: id, content: content, views: [], created_at: Time.now.utc }
|
|
39
|
+
@situations[id]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def generate_view(situation_id:, perspective_id:, valence: 0.0,
|
|
43
|
+
concerns: [], opportunities: [], confidence: 0.5)
|
|
44
|
+
situation = @situations[situation_id]
|
|
45
|
+
perspective = @perspectives[perspective_id]
|
|
46
|
+
|
|
47
|
+
return { error: :situation_not_found } unless situation
|
|
48
|
+
return { error: :perspective_not_found } unless perspective
|
|
49
|
+
return { error: :too_many_views } if situation[:views].size >= Constants::MAX_VIEWS_PER_SITUATION
|
|
50
|
+
|
|
51
|
+
view = PerspectiveView.new(
|
|
52
|
+
situation_id: situation_id,
|
|
53
|
+
perspective_id: perspective_id,
|
|
54
|
+
valence: valence,
|
|
55
|
+
concerns: concerns,
|
|
56
|
+
opportunities: opportunities,
|
|
57
|
+
confidence: confidence
|
|
58
|
+
)
|
|
59
|
+
situation[:views] << view
|
|
60
|
+
view
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def views_for_situation(situation_id:)
|
|
64
|
+
situation = @situations[situation_id]
|
|
65
|
+
return [] unless situation
|
|
66
|
+
|
|
67
|
+
situation[:views]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def perspective_agreement(situation_id:)
|
|
71
|
+
views = views_for_situation(situation_id: situation_id)
|
|
72
|
+
return 0.0 if views.size < Constants::MIN_PERSPECTIVES_FOR_SYNTHESIS
|
|
73
|
+
|
|
74
|
+
valences = views.map(&:valence)
|
|
75
|
+
mean = valences.sum / valences.size.to_f
|
|
76
|
+
variance = valences.sum { |v| (v - mean)**2 } / valences.size.to_f
|
|
77
|
+
std_dev = Math.sqrt(variance)
|
|
78
|
+
|
|
79
|
+
# Agreement is inverse of normalized std_dev (max std_dev is 1.0 for symmetric ±1 distribution)
|
|
80
|
+
(1.0 - std_dev).clamp(0.0, 1.0).round(10)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def blind_spots(situation_id:)
|
|
84
|
+
views = views_for_situation(situation_id: situation_id)
|
|
85
|
+
applied_ids = views.map(&:perspective_id).uniq
|
|
86
|
+
applied_types = applied_ids.filter_map { |pid| @perspectives[pid]&.perspective_type }.uniq
|
|
87
|
+
Constants::PERSPECTIVE_TYPES - applied_types
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def coverage_score(situation_id:)
|
|
91
|
+
views = views_for_situation(situation_id: situation_id)
|
|
92
|
+
return 0.0 if @perspectives.empty?
|
|
93
|
+
|
|
94
|
+
applied_count = views.map(&:perspective_id).uniq.size
|
|
95
|
+
(applied_count.to_f / @perspectives.size).clamp(0.0, 1.0).round(10)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def dominant_view(situation_id:)
|
|
99
|
+
views = views_for_situation(situation_id: situation_id)
|
|
100
|
+
return nil if views.empty?
|
|
101
|
+
|
|
102
|
+
views.max_by(&:confidence)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def synthesize(situation_id:)
|
|
106
|
+
views = views_for_situation(situation_id: situation_id)
|
|
107
|
+
return { error: :insufficient_views } if views.size < Constants::MIN_PERSPECTIVES_FOR_SYNTHESIS
|
|
108
|
+
|
|
109
|
+
total_confidence = views.sum(&:confidence)
|
|
110
|
+
return { error: :zero_confidence } if total_confidence.zero?
|
|
111
|
+
|
|
112
|
+
weighted_valence = views.sum { |v| v.valence * v.confidence } / total_confidence
|
|
113
|
+
all_concerns = views.flat_map(&:concerns).uniq
|
|
114
|
+
all_opportunities = views.flat_map(&:opportunities).uniq
|
|
115
|
+
agreement = perspective_agreement(situation_id: situation_id)
|
|
116
|
+
|
|
117
|
+
{
|
|
118
|
+
situation_id: situation_id,
|
|
119
|
+
weighted_valence: weighted_valence.round(10),
|
|
120
|
+
concerns: all_concerns,
|
|
121
|
+
opportunities: all_opportunities,
|
|
122
|
+
agreement: agreement.round(10),
|
|
123
|
+
agreement_label: Constants.agreement_label(agreement),
|
|
124
|
+
view_count: views.size,
|
|
125
|
+
coverage_score: coverage_score(situation_id: situation_id),
|
|
126
|
+
coverage_label: Constants.coverage_label(coverage_score(situation_id: situation_id))
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def most_divergent_pair(situation_id:)
|
|
131
|
+
views = views_for_situation(situation_id: situation_id)
|
|
132
|
+
return nil if views.size < 2
|
|
133
|
+
|
|
134
|
+
max_diff = -1.0
|
|
135
|
+
pair = nil
|
|
136
|
+
|
|
137
|
+
views.combination(2) do |a, b|
|
|
138
|
+
diff = (a.valence - b.valence).abs
|
|
139
|
+
if diff > max_diff
|
|
140
|
+
max_diff = diff
|
|
141
|
+
pair = [a, b]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
pair
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def to_h
|
|
149
|
+
{
|
|
150
|
+
perspective_count: @perspectives.size,
|
|
151
|
+
situation_count: @situations.size,
|
|
152
|
+
perspectives: @perspectives.values.map(&:to_h),
|
|
153
|
+
situations: @situations.values.map do |s|
|
|
154
|
+
s.merge(views: s[:views].map(&:to_h))
|
|
155
|
+
end
|
|
156
|
+
}
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module PerspectiveShifting
|
|
8
|
+
module Runners
|
|
9
|
+
module PerspectiveShifting
|
|
10
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
11
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
12
|
+
|
|
13
|
+
# --- Perspective management ---
|
|
14
|
+
|
|
15
|
+
def add_perspective(name:, type: :stakeholder, priorities: [], empathy: Helpers::Constants::DEFAULT_EMPATHY,
|
|
16
|
+
bias: nil, expertise_domains: [], engine: nil, **)
|
|
17
|
+
eng = engine || shifting_engine
|
|
18
|
+
result = eng.add_perspective(name: name, type: type, priorities: priorities,
|
|
19
|
+
empathy: empathy, bias: bias, expertise_domains: expertise_domains)
|
|
20
|
+
return { success: false, error: result[:error] } if result.is_a?(Hash) && result[:error]
|
|
21
|
+
|
|
22
|
+
Legion::Logging.debug "[perspective_shifting] added perspective name=#{name} type=#{type} id=#{result.id[0..7]}"
|
|
23
|
+
{ success: true, perspective: result.to_h }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def list_perspectives(engine: nil, **)
|
|
27
|
+
eng = engine || shifting_engine
|
|
28
|
+
persp = eng.perspectives.values.map(&:to_h)
|
|
29
|
+
Legion::Logging.debug "[perspective_shifting] list_perspectives count=#{persp.size}"
|
|
30
|
+
{ success: true, perspectives: persp, count: persp.size }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def get_perspective(perspective_id:, engine: nil, **)
|
|
34
|
+
eng = engine || shifting_engine
|
|
35
|
+
persp = eng.perspectives[perspective_id]
|
|
36
|
+
if persp
|
|
37
|
+
{ success: true, found: true, perspective: persp.to_h }
|
|
38
|
+
else
|
|
39
|
+
Legion::Logging.debug "[perspective_shifting] perspective not found id=#{perspective_id[0..7]}"
|
|
40
|
+
{ success: false, found: false }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# --- Situation management ---
|
|
45
|
+
|
|
46
|
+
def add_situation(content:, engine: nil, **)
|
|
47
|
+
eng = engine || shifting_engine
|
|
48
|
+
result = eng.add_situation(content: content)
|
|
49
|
+
return { success: false, error: result[:error] } if result.is_a?(Hash) && result[:error]
|
|
50
|
+
|
|
51
|
+
Legion::Logging.debug "[perspective_shifting] added situation id=#{result[:id][0..7]}"
|
|
52
|
+
{ success: true, situation_id: result[:id], content: result[:content] }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def list_situations(engine: nil, **)
|
|
56
|
+
eng = engine || shifting_engine
|
|
57
|
+
sits = eng.situations.values.map { |s| s.merge(views: s[:views].map(&:to_h)) }
|
|
58
|
+
Legion::Logging.debug "[perspective_shifting] list_situations count=#{sits.size}"
|
|
59
|
+
{ success: true, situations: sits, count: sits.size }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# --- View generation and retrieval ---
|
|
63
|
+
|
|
64
|
+
def generate_view(situation_id:, perspective_id:, valence: 0.0,
|
|
65
|
+
concerns: [], opportunities: [], confidence: 0.5, engine: nil, **)
|
|
66
|
+
eng = engine || shifting_engine
|
|
67
|
+
result = eng.generate_view(
|
|
68
|
+
situation_id: situation_id,
|
|
69
|
+
perspective_id: perspective_id,
|
|
70
|
+
valence: valence,
|
|
71
|
+
concerns: concerns,
|
|
72
|
+
opportunities: opportunities,
|
|
73
|
+
confidence: confidence
|
|
74
|
+
)
|
|
75
|
+
return { success: false, error: result[:error] } if result.is_a?(Hash) && result[:error]
|
|
76
|
+
|
|
77
|
+
Legion::Logging.debug "[perspective_shifting] generated view id=#{result.id[0..7]} " \
|
|
78
|
+
"valence=#{result.valence.round(2)}"
|
|
79
|
+
{ success: true, view: result.to_h }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def views_for_situation(situation_id:, engine: nil, **)
|
|
83
|
+
eng = engine || shifting_engine
|
|
84
|
+
views = eng.views_for_situation(situation_id: situation_id)
|
|
85
|
+
Legion::Logging.debug "[perspective_shifting] views_for_situation id=#{situation_id[0..7]} count=#{views.size}"
|
|
86
|
+
{ success: true, views: views.map(&:to_h), count: views.size }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# --- Analysis ---
|
|
90
|
+
|
|
91
|
+
def perspective_agreement(situation_id:, engine: nil, **)
|
|
92
|
+
eng = engine || shifting_engine
|
|
93
|
+
score = eng.perspective_agreement(situation_id: situation_id)
|
|
94
|
+
label = Helpers::Constants.agreement_label(score)
|
|
95
|
+
Legion::Logging.debug "[perspective_shifting] agreement situation=#{situation_id[0..7]} score=#{score.round(2)} label=#{label}"
|
|
96
|
+
{ success: true, situation_id: situation_id, agreement: score, label: label }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def blind_spots(situation_id:, engine: nil, **)
|
|
100
|
+
eng = engine || shifting_engine
|
|
101
|
+
spots = eng.blind_spots(situation_id: situation_id)
|
|
102
|
+
Legion::Logging.debug "[perspective_shifting] blind_spots situation=#{situation_id[0..7]} count=#{spots.size}"
|
|
103
|
+
{ success: true, situation_id: situation_id, blind_spots: spots, count: spots.size }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def coverage_score(situation_id:, engine: nil, **)
|
|
107
|
+
eng = engine || shifting_engine
|
|
108
|
+
score = eng.coverage_score(situation_id: situation_id)
|
|
109
|
+
label = Helpers::Constants.coverage_label(score)
|
|
110
|
+
Legion::Logging.debug "[perspective_shifting] coverage situation=#{situation_id[0..7]} score=#{score.round(2)} label=#{label}"
|
|
111
|
+
{ success: true, situation_id: situation_id, coverage: score, label: label }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def dominant_view(situation_id:, engine: nil, **)
|
|
115
|
+
eng = engine || shifting_engine
|
|
116
|
+
view = eng.dominant_view(situation_id: situation_id)
|
|
117
|
+
if view
|
|
118
|
+
Legion::Logging.debug "[perspective_shifting] dominant_view situation=#{situation_id[0..7]} confidence=#{view.confidence.round(2)}"
|
|
119
|
+
{ success: true, found: true, view: view.to_h }
|
|
120
|
+
else
|
|
121
|
+
{ success: true, found: false }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def synthesize(situation_id:, engine: nil, **)
|
|
126
|
+
eng = engine || shifting_engine
|
|
127
|
+
result = eng.synthesize(situation_id: situation_id)
|
|
128
|
+
return { success: false, error: result[:error] } if result[:error]
|
|
129
|
+
|
|
130
|
+
Legion::Logging.info "[perspective_shifting] synthesized situation=#{situation_id[0..7]} " \
|
|
131
|
+
"valence=#{result[:weighted_valence].round(2)} views=#{result[:view_count]}"
|
|
132
|
+
result.merge(success: true)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def most_divergent_pair(situation_id:, engine: nil, **)
|
|
136
|
+
eng = engine || shifting_engine
|
|
137
|
+
pair = eng.most_divergent_pair(situation_id: situation_id)
|
|
138
|
+
unless pair
|
|
139
|
+
Legion::Logging.debug '[perspective_shifting] most_divergent_pair: not enough views'
|
|
140
|
+
return { success: true, found: false }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
divergence = (pair[0].valence - pair[1].valence).abs.round(10)
|
|
144
|
+
Legion::Logging.debug "[perspective_shifting] most_divergent_pair divergence=#{divergence.round(2)}"
|
|
145
|
+
{ success: true, found: true, views: pair.map(&:to_h), divergence: divergence }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def engine_status(engine: nil, **)
|
|
149
|
+
eng = engine || shifting_engine
|
|
150
|
+
Legion::Logging.debug '[perspective_shifting] engine_status'
|
|
151
|
+
{ success: true }.merge(eng.to_h)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def shifting_engine
|
|
157
|
+
@shifting_engine ||= Helpers::ShiftingEngine.new
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/perspective_shifting/version'
|
|
4
|
+
require 'legion/extensions/perspective_shifting/helpers/constants'
|
|
5
|
+
require 'legion/extensions/perspective_shifting/helpers/perspective'
|
|
6
|
+
require 'legion/extensions/perspective_shifting/helpers/perspective_view'
|
|
7
|
+
require 'legion/extensions/perspective_shifting/helpers/shifting_engine'
|
|
8
|
+
require 'legion/extensions/perspective_shifting/runners/perspective_shifting'
|
|
9
|
+
|
|
10
|
+
module Legion
|
|
11
|
+
module Extensions
|
|
12
|
+
module PerspectiveShifting
|
|
13
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/perspective_shifting/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::PerspectiveShifting::Client do
|
|
6
|
+
it 'responds to all runner methods' do
|
|
7
|
+
client = described_class.new
|
|
8
|
+
expect(client).to respond_to(:add_perspective)
|
|
9
|
+
expect(client).to respond_to(:list_perspectives)
|
|
10
|
+
expect(client).to respond_to(:get_perspective)
|
|
11
|
+
expect(client).to respond_to(:add_situation)
|
|
12
|
+
expect(client).to respond_to(:list_situations)
|
|
13
|
+
expect(client).to respond_to(:generate_view)
|
|
14
|
+
expect(client).to respond_to(:views_for_situation)
|
|
15
|
+
expect(client).to respond_to(:perspective_agreement)
|
|
16
|
+
expect(client).to respond_to(:blind_spots)
|
|
17
|
+
expect(client).to respond_to(:coverage_score)
|
|
18
|
+
expect(client).to respond_to(:dominant_view)
|
|
19
|
+
expect(client).to respond_to(:synthesize)
|
|
20
|
+
expect(client).to respond_to(:most_divergent_pair)
|
|
21
|
+
expect(client).to respond_to(:engine_status)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'initializes with its own isolated engine per instance' do
|
|
25
|
+
c1 = described_class.new
|
|
26
|
+
c2 = described_class.new
|
|
27
|
+
c1.add_perspective(name: 'CEO', type: :stakeholder)
|
|
28
|
+
expect(c2.list_perspectives[:count]).to eq(0)
|
|
29
|
+
end
|
|
30
|
+
end
|