lex-cognitive-empathy 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/cognitive_empathy/client.rb +15 -0
- data/lib/legion/extensions/cognitive_empathy/helpers/constants.rb +33 -0
- data/lib/legion/extensions/cognitive_empathy/helpers/empathy_engine.rb +147 -0
- data/lib/legion/extensions/cognitive_empathy/helpers/perspective.rb +88 -0
- data/lib/legion/extensions/cognitive_empathy/runners/cognitive_empathy.rb +89 -0
- data/lib/legion/extensions/cognitive_empathy/version.rb +9 -0
- metadata +67 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1f590431e81d898d029f4728f145da572298e5d07f3a7b9c105d0ffd05519a8c
|
|
4
|
+
data.tar.gz: e7ad498fdb8d1ae424fd5cac5b3463031358e0b74a6573f8d970c4dac68a042e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e5bcfb9c8f0a05cc9bdea9d19fc031a461cb4ae5b05a159ee22eedfbb671797dde95ab424b94e804f18eb208bb78e73f8476642df35fc4764568b395a003f90b
|
|
7
|
+
data.tar.gz: 659247e264da0939ade120322f814014b604c966ec4cac8921b46bfae5d1deca84ccfc5c263a51693d432a61fa93efee60f4fb0de06f5c1c8150375b769bbdf2
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveEmpathy
|
|
6
|
+
class Client
|
|
7
|
+
include Runners::CognitiveEmpathy
|
|
8
|
+
|
|
9
|
+
def initialize(engine: nil)
|
|
10
|
+
@engine = engine || Helpers::EmpathyEngine.new
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveEmpathy
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
MAX_PERSPECTIVES = 50
|
|
9
|
+
MAX_INTERACTIONS = 200
|
|
10
|
+
MAX_HISTORY = 200
|
|
11
|
+
|
|
12
|
+
DEFAULT_ACCURACY = 0.5
|
|
13
|
+
ACCURACY_FLOOR = 0.1
|
|
14
|
+
ACCURACY_CEILING = 0.95
|
|
15
|
+
CONTAGION_RATE = 0.15
|
|
16
|
+
CONTAGION_DECAY = 0.05
|
|
17
|
+
ACCURACY_ALPHA = 0.1
|
|
18
|
+
|
|
19
|
+
PERSPECTIVE_TYPES = %i[cognitive affective motivational situational].freeze
|
|
20
|
+
EMPATHIC_STATES = %i[detached observing resonating immersed].freeze
|
|
21
|
+
|
|
22
|
+
ACCURACY_LABELS = {
|
|
23
|
+
(0.8..) => :excellent,
|
|
24
|
+
(0.6...0.8) => :good,
|
|
25
|
+
(0.4...0.6) => :moderate,
|
|
26
|
+
(0.2...0.4) => :poor,
|
|
27
|
+
(..0.2) => :blind
|
|
28
|
+
}.freeze
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveEmpathy
|
|
6
|
+
module Helpers
|
|
7
|
+
class EmpathyEngine
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :perspectives, :contagion_level, :history
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@perspectives = {}
|
|
14
|
+
@contagion_level = 0.0
|
|
15
|
+
@counter = 0
|
|
16
|
+
@history = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def take_perspective(agent_id:, perspective_type:, predicted_state:, confidence:)
|
|
20
|
+
return nil if @perspectives.size >= MAX_PERSPECTIVES
|
|
21
|
+
return nil unless PERSPECTIVE_TYPES.include?(perspective_type)
|
|
22
|
+
|
|
23
|
+
@counter += 1
|
|
24
|
+
id = :"perspective_#{@counter}"
|
|
25
|
+
perspective = Perspective.new(
|
|
26
|
+
id: id,
|
|
27
|
+
agent_id: agent_id,
|
|
28
|
+
perspective_type: perspective_type,
|
|
29
|
+
predicted_state: predicted_state,
|
|
30
|
+
confidence: confidence
|
|
31
|
+
)
|
|
32
|
+
@perspectives[id] = perspective
|
|
33
|
+
record_event(:perspective_taken, id: id, agent_id: agent_id)
|
|
34
|
+
perspective
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def record_outcome(perspective_id:, actual_state:)
|
|
38
|
+
perspective = @perspectives[perspective_id]
|
|
39
|
+
return nil unless perspective
|
|
40
|
+
|
|
41
|
+
perspective.record_actual(actual_state: actual_state)
|
|
42
|
+
record_event(:outcome_recorded, id: perspective_id, accuracy: perspective.accuracy)
|
|
43
|
+
perspective
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def empathic_accuracy(agent_id:)
|
|
47
|
+
agent_perspectives = perspectives_for(agent_id: agent_id)
|
|
48
|
+
resolved = agent_perspectives.select(&:resolved?)
|
|
49
|
+
return DEFAULT_ACCURACY if resolved.empty?
|
|
50
|
+
|
|
51
|
+
resolved.sum(&:accuracy) / resolved.size
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def overall_accuracy
|
|
55
|
+
resolved = @perspectives.values.select(&:resolved?)
|
|
56
|
+
return DEFAULT_ACCURACY if resolved.empty?
|
|
57
|
+
|
|
58
|
+
resolved.sum(&:accuracy) / resolved.size
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def emotional_contagion(emotion_valence:, intensity:)
|
|
62
|
+
intensity_f = intensity.to_f.clamp(0.0, 1.0)
|
|
63
|
+
absorption = CONTAGION_RATE * intensity_f
|
|
64
|
+
@contagion_level = (@contagion_level + absorption).clamp(0.0, 1.0)
|
|
65
|
+
record_event(:contagion, valence: emotion_valence, intensity: intensity_f,
|
|
66
|
+
level: @contagion_level)
|
|
67
|
+
@contagion_level
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def contagion_decay
|
|
71
|
+
@contagion_level = [@contagion_level - CONTAGION_DECAY, 0.0].max
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def empathic_state
|
|
75
|
+
return :immersed if @contagion_level >= 0.75
|
|
76
|
+
return :resonating if @contagion_level >= 0.45
|
|
77
|
+
return :observing if @contagion_level >= 0.15
|
|
78
|
+
|
|
79
|
+
:detached
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def perspectives_for(agent_id:)
|
|
83
|
+
@perspectives.values.select { |p| p.agent_id == agent_id }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def most_accurate_agent
|
|
87
|
+
agent_accuracies = build_agent_accuracies
|
|
88
|
+
return nil if agent_accuracies.empty?
|
|
89
|
+
|
|
90
|
+
agent_accuracies.max_by { |_, acc| acc }&.first
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def least_accurate_agent
|
|
94
|
+
agent_accuracies = build_agent_accuracies
|
|
95
|
+
return nil if agent_accuracies.empty?
|
|
96
|
+
|
|
97
|
+
agent_accuracies.min_by { |_, acc| acc }&.first
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def tick
|
|
101
|
+
contagion_decay
|
|
102
|
+
prune_old_perspectives
|
|
103
|
+
self
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def to_h
|
|
107
|
+
{
|
|
108
|
+
perspective_count: @perspectives.size,
|
|
109
|
+
resolved_count: @perspectives.values.count(&:resolved?),
|
|
110
|
+
overall_accuracy: overall_accuracy.round(4),
|
|
111
|
+
contagion_level: @contagion_level.round(4),
|
|
112
|
+
empathic_state: empathic_state,
|
|
113
|
+
history_size: @history.size
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def build_agent_accuracies
|
|
120
|
+
agent_ids = @perspectives.values.map(&:agent_id).uniq
|
|
121
|
+
accuracies = {}
|
|
122
|
+
agent_ids.each do |aid|
|
|
123
|
+
resolved = perspectives_for(agent_id: aid).select(&:resolved?)
|
|
124
|
+
next if resolved.empty?
|
|
125
|
+
|
|
126
|
+
accuracies[aid] = resolved.sum(&:accuracy) / resolved.size
|
|
127
|
+
end
|
|
128
|
+
accuracies
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def prune_old_perspectives
|
|
132
|
+
resolved = @perspectives.select { |_, p| p.resolved? }
|
|
133
|
+
return unless resolved.size > MAX_PERSPECTIVES / 2
|
|
134
|
+
|
|
135
|
+
oldest_keys = resolved.keys.first(resolved.size - (MAX_PERSPECTIVES / 4))
|
|
136
|
+
oldest_keys.each { |k| @perspectives.delete(k) }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def record_event(type, **details)
|
|
140
|
+
@history << { type: type, at: Time.now.utc }.merge(details)
|
|
141
|
+
@history.shift while @history.size > MAX_HISTORY
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveEmpathy
|
|
6
|
+
module Helpers
|
|
7
|
+
class Perspective
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :id, :agent_id, :perspective_type, :predicted_state, :actual_state,
|
|
11
|
+
:confidence, :accuracy
|
|
12
|
+
|
|
13
|
+
def initialize(id:, agent_id:, perspective_type: :cognitive, predicted_state: {}, confidence: 0.5)
|
|
14
|
+
@id = id
|
|
15
|
+
@agent_id = agent_id
|
|
16
|
+
@perspective_type = perspective_type
|
|
17
|
+
@predicted_state = predicted_state
|
|
18
|
+
@confidence = confidence.to_f.clamp(0.0, 1.0)
|
|
19
|
+
@actual_state = nil
|
|
20
|
+
@accuracy = DEFAULT_ACCURACY
|
|
21
|
+
@created_at = Time.now.utc
|
|
22
|
+
@resolved_at = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def record_actual(actual_state:)
|
|
26
|
+
@actual_state = actual_state
|
|
27
|
+
@resolved_at = Time.now.utc
|
|
28
|
+
error = compute_error(predicted_state, actual_state)
|
|
29
|
+
observed_accuracy = (1.0 - error).clamp(ACCURACY_FLOOR, ACCURACY_CEILING)
|
|
30
|
+
@accuracy = ((1.0 - ACCURACY_ALPHA) * @accuracy) + (ACCURACY_ALPHA * observed_accuracy)
|
|
31
|
+
@accuracy = @accuracy.clamp(ACCURACY_FLOOR, ACCURACY_CEILING)
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def accurate?
|
|
36
|
+
@accuracy > 0.6
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def resolved?
|
|
40
|
+
!@actual_state.nil?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_h
|
|
44
|
+
{
|
|
45
|
+
id: @id,
|
|
46
|
+
agent_id: @agent_id,
|
|
47
|
+
perspective_type: @perspective_type,
|
|
48
|
+
predicted_state: @predicted_state,
|
|
49
|
+
actual_state: @actual_state,
|
|
50
|
+
confidence: @confidence.round(4),
|
|
51
|
+
accuracy: @accuracy.round(4),
|
|
52
|
+
accurate: accurate?,
|
|
53
|
+
resolved: resolved?,
|
|
54
|
+
created_at: @created_at,
|
|
55
|
+
resolved_at: @resolved_at
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def compute_error(predicted, actual)
|
|
62
|
+
return 0.0 if predicted.empty? && actual.empty?
|
|
63
|
+
return 1.0 if predicted.empty? || actual.empty?
|
|
64
|
+
|
|
65
|
+
keys = (predicted.keys | actual.keys)
|
|
66
|
+
return 1.0 if keys.empty?
|
|
67
|
+
|
|
68
|
+
total_error = keys.sum do |k|
|
|
69
|
+
p_val = numeric_value(predicted[k])
|
|
70
|
+
a_val = numeric_value(actual[k])
|
|
71
|
+
(p_val - a_val).abs
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
(total_error / keys.size).clamp(0.0, 1.0)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def numeric_value(val)
|
|
78
|
+
return val.to_f if val.is_a?(Numeric)
|
|
79
|
+
return 1.0 if val == true
|
|
80
|
+
return 0.0 if val == false || val.nil?
|
|
81
|
+
|
|
82
|
+
0.5
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveEmpathy
|
|
6
|
+
module Runners
|
|
7
|
+
module CognitiveEmpathy
|
|
8
|
+
include Helpers::Constants
|
|
9
|
+
include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
|
|
10
|
+
|
|
11
|
+
def take_empathic_perspective(agent_id:, perspective_type:, predicted_state:, confidence: 0.5, **)
|
|
12
|
+
perspective = engine.take_perspective(
|
|
13
|
+
agent_id: agent_id,
|
|
14
|
+
perspective_type: perspective_type,
|
|
15
|
+
predicted_state: predicted_state,
|
|
16
|
+
confidence: confidence
|
|
17
|
+
)
|
|
18
|
+
return { success: false, reason: :limit_or_invalid_type } unless perspective
|
|
19
|
+
|
|
20
|
+
{ success: true, perspective_id: perspective.id, agent_id: agent_id,
|
|
21
|
+
perspective_type: perspective_type }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def record_empathic_outcome(perspective_id:, actual_state:, **)
|
|
25
|
+
perspective = engine.record_outcome(perspective_id: perspective_id, actual_state: actual_state)
|
|
26
|
+
return { success: false, reason: :not_found } unless perspective
|
|
27
|
+
|
|
28
|
+
{ success: true, perspective_id: perspective_id,
|
|
29
|
+
accuracy: perspective.accuracy.round(4), accurate: perspective.accurate? }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def empathic_accuracy_for(agent_id:, **)
|
|
33
|
+
accuracy = engine.empathic_accuracy(agent_id: agent_id)
|
|
34
|
+
label = accuracy_label(accuracy)
|
|
35
|
+
{ success: true, agent_id: agent_id, accuracy: accuracy.round(4), label: label }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def overall_empathic_accuracy(**)
|
|
39
|
+
accuracy = engine.overall_accuracy
|
|
40
|
+
label = accuracy_label(accuracy)
|
|
41
|
+
{ success: true, accuracy: accuracy.round(4), label: label }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def apply_emotional_contagion(emotion_valence:, intensity:, **)
|
|
45
|
+
level = engine.emotional_contagion(emotion_valence: emotion_valence, intensity: intensity)
|
|
46
|
+
{ success: true, contagion_level: level.round(4), empathic_state: engine.empathic_state }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def current_empathic_state(**)
|
|
50
|
+
{ success: true, empathic_state: engine.empathic_state,
|
|
51
|
+
contagion_level: engine.contagion_level.round(4) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def perspectives_for_agent(agent_id:, **)
|
|
55
|
+
list = engine.perspectives_for(agent_id: agent_id).map(&:to_h)
|
|
56
|
+
{ success: true, agent_id: agent_id, perspectives: list, count: list.size }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def empathic_blind_spots(**)
|
|
60
|
+
least = engine.least_accurate_agent
|
|
61
|
+
most = engine.most_accurate_agent
|
|
62
|
+
{ success: true, least_accurate_agent: least, most_accurate_agent: most,
|
|
63
|
+
overall_accuracy: engine.overall_accuracy.round(4) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def update_cognitive_empathy(**)
|
|
67
|
+
engine.tick
|
|
68
|
+
{ success: true }.merge(engine.to_h)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def cognitive_empathy_stats(**)
|
|
72
|
+
{ success: true }.merge(engine.to_h)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def engine
|
|
78
|
+
@engine ||= Helpers::EmpathyEngine.new
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def accuracy_label(accuracy)
|
|
82
|
+
ACCURACY_LABELS.each { |range, lbl| return lbl if range.cover?(accuracy) }
|
|
83
|
+
:blind
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-cognitive-empathy
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Esity
|
|
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: Perspective-taking, emotional contagion modeling, and empathic accuracy
|
|
27
|
+
tracking for LegionIO agentic extensions
|
|
28
|
+
email:
|
|
29
|
+
- matthewdiverson@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- lib/legion/extensions/cognitive_empathy/client.rb
|
|
35
|
+
- lib/legion/extensions/cognitive_empathy/helpers/constants.rb
|
|
36
|
+
- lib/legion/extensions/cognitive_empathy/helpers/empathy_engine.rb
|
|
37
|
+
- lib/legion/extensions/cognitive_empathy/helpers/perspective.rb
|
|
38
|
+
- lib/legion/extensions/cognitive_empathy/runners/cognitive_empathy.rb
|
|
39
|
+
- lib/legion/extensions/cognitive_empathy/version.rb
|
|
40
|
+
homepage: https://github.com/LegionIO/lex-cognitive-empathy
|
|
41
|
+
licenses:
|
|
42
|
+
- MIT
|
|
43
|
+
metadata:
|
|
44
|
+
homepage_uri: https://github.com/LegionIO/lex-cognitive-empathy
|
|
45
|
+
source_code_uri: https://github.com/LegionIO/lex-cognitive-empathy
|
|
46
|
+
documentation_uri: https://github.com/LegionIO/lex-cognitive-empathy/blob/master/README.md
|
|
47
|
+
changelog_uri: https://github.com/LegionIO/lex-cognitive-empathy/blob/master/CHANGELOG.md
|
|
48
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-cognitive-empathy/issues
|
|
49
|
+
rubygems_mfa_required: 'true'
|
|
50
|
+
rdoc_options: []
|
|
51
|
+
require_paths:
|
|
52
|
+
- lib
|
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: '3.4'
|
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '0'
|
|
63
|
+
requirements: []
|
|
64
|
+
rubygems_version: 3.6.9
|
|
65
|
+
specification_version: 4
|
|
66
|
+
summary: Cognitive empathy engine for LegionIO
|
|
67
|
+
test_files: []
|