awesome_chatgpt_actors 0.1.1 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -3
- data/Gemfile.lock +4 -6
- data/README.md +60 -17
- data/awesome_chatgpt_actors.gemspec +2 -3
- data/lib/awesome_chatgpt_actors/actor.rb +22 -36
- data/lib/awesome_chatgpt_actors/cast_control.rb +50 -10
- data/lib/awesome_chatgpt_actors/version.rb +2 -2
- data/lib/awesome_chatgpt_actors.rb +0 -1
- data/lib/tasks/chatgpt_actors.rake +16 -7
- data/prompts-data/{awesome-chatgpt-prompts.csv → awesome-en-prompts.csv} +164 -164
- data/prompts-data/awesome-pt-prompts.csv +164 -0
- metadata +9 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bb98a39fa29a5a112ba5072ed224c3bcad7fcd191bf22292228489706640ded8
|
4
|
+
data.tar.gz: 50e5314d33d6076d7480270085f2e38a533f53ae2777274d564be6310dd91a95
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 91e38acb91ebb2a453b1df7691dbd43a7c0f7c12952ba2c9c62ad104f6af34f8a584a15189ef6d4af4ff4e5a717ebfc53d32df568754ffd1cc5ac894af1e46d1
|
7
|
+
data.tar.gz: 2ebc58e6212bc50577b1e513ebf5d1254d78747a615885c1a80ae90709bb6881ce95989d550b8b5d219562f0c58f2075a6e6964d01e955f61170b45cff864190
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
awesome_chatgpt_actors (0.1.
|
5
|
-
csv
|
6
|
-
openai
|
4
|
+
awesome_chatgpt_actors (0.1.3)
|
5
|
+
csv (~> 3.1)
|
7
6
|
|
8
7
|
GEM
|
9
8
|
remote: https://rubygems.org/
|
@@ -111,7 +110,6 @@ GEM
|
|
111
110
|
nio4r (2.5.9)
|
112
111
|
nokogiri (1.14.3-x86_64-linux)
|
113
112
|
racc (~> 1.4)
|
114
|
-
openai (0.3.0)
|
115
113
|
parallel (1.22.1)
|
116
114
|
parser (3.2.2.0)
|
117
115
|
ast (~> 2.4.1)
|
@@ -192,12 +190,12 @@ GEM
|
|
192
190
|
zeitwerk (2.6.7)
|
193
191
|
|
194
192
|
PLATFORMS
|
193
|
+
ruby
|
195
194
|
x86_64-linux
|
196
195
|
|
197
196
|
DEPENDENCIES
|
198
197
|
awesome_chatgpt_actors!
|
199
|
-
csv
|
200
|
-
openai
|
198
|
+
csv (~> 3.1)
|
201
199
|
rails
|
202
200
|
rake (~> 13.0)
|
203
201
|
rspec (~> 3.0)
|
data/README.md
CHANGED
@@ -1,7 +1,12 @@
|
|
1
|
-
#
|
1
|
+
# Awesome ChatGPT Actors
|
2
2
|
|
3
|
-
This gem is a collection of actors made
|
3
|
+
This gem is a collection of actors made with [Awesome Chatgpt Prompts](https://github.com/f/awesome-chatgpt-prompts) in ruby.
|
4
4
|
So for prompts information, please refer to that repository.
|
5
|
+
The prompts was edited and translated to brazillian portuguese, so it can be used in english and portuguese.
|
6
|
+
The actors is currently been tested, and it's default values will be changed in the future.
|
7
|
+
|
8
|
+
## Requirements
|
9
|
+
- Ruby 2.6 or higher
|
5
10
|
|
6
11
|
## Installation
|
7
12
|
|
@@ -27,67 +32,105 @@ Run in the command line:
|
|
27
32
|
```ruby
|
28
33
|
require 'awesome_chatgpt_actors'
|
29
34
|
|
30
|
-
# Initialize a new actor
|
35
|
+
# Initialize a new actor with default options
|
31
36
|
actor = AwesomeChatgptActors::Actor.new
|
32
37
|
|
33
38
|
# it will return an actor with the following options
|
34
39
|
# {
|
35
|
-
#
|
36
|
-
# prompt: 'I want you to act as a virtual assistant. I will give you tasks and you will reply with the results of the tasks. I want you to only reply with the results of the tasks, and nothing else. Do not write explanations. My first task is ""I want you to write a short story about a cat"""
|
40
|
+
# role: 'Virtual Assistant',
|
41
|
+
# prompt: 'I want you to act as a virtual assistant. I will give you tasks and you will reply with the results of the tasks. I want you to only reply with the results of the tasks, and nothing else. Do not write explanations. My first task is ""I want you to write a short story about a cat""",
|
42
|
+
# actors_csv: csv_path_of_current_actor,
|
43
|
+
# accertivity: 1, # 1 is the most accertive, 2 is the least accertiv, this is called as temperature in openai
|
44
|
+
# default_frequency: 0, # can be changed if you define a default prompt to when instance is initialized
|
45
|
+
# default_presence: 0, # can be changed if you define a default prompt to when instance is initialized
|
46
|
+
# has_placeholder: false, # has a placeholder in the prompt or role to be replaced by the user input
|
47
|
+
# language: 'en', # pt for portuguese, en for english, it can be changed even if you don't provide an default prompt
|
37
48
|
# }
|
38
49
|
|
39
50
|
# Initialize a new actor with options
|
40
51
|
actor = AwesomeChatgptActors::Actor.new(
|
41
|
-
|
42
|
-
language: 'en', # The language of the prompt, if other language is specified, it will requirean openai api key to perform the translation
|
52
|
+
role: 'Virtual Assistant', # DEFAULT. Not required. The type of actor to use, check the list in https://github.com/f/awesome-chatgpt-prompts/blob/main/prompts.csv
|
53
|
+
language: 'en', # DEFAULT. Not required. The language of the prompt, if other language is specified, it will requirean openai api key to perform the translation
|
43
54
|
)
|
44
55
|
|
45
|
-
# Initialize a new actor with
|
56
|
+
# Initialize a new actor with othe acceptable language
|
46
57
|
actor = AwesomeChatgptActors::Actor.new(
|
47
58
|
language: 'pt',
|
48
|
-
openai_api_key: 'Your OpenAI API Key' # it can be nil if you provide it as an environment variable OPENAI_API_KEY
|
49
59
|
)
|
50
60
|
|
51
|
-
# Initialize a new actor with
|
61
|
+
# Initialize a new actor with a different role and language
|
52
62
|
actor = AwesomeChatgptActors::Actor.new(
|
53
|
-
|
63
|
+
role: 'Virtual Assistant',
|
64
|
+
language: 'pt',
|
65
|
+
)
|
66
|
+
|
67
|
+
# Initialize a new actor with a different attributes
|
68
|
+
## To change the attributes, you need to initialize the actor with a different prompt or it will use the default prompt of the role
|
69
|
+
actor = AwesomeChatgptActors::Actor.new(
|
70
|
+
role: 'Virtual Assistant',
|
71
|
+
language: 'en',
|
72
|
+
prompt: 'I want you to act as a virtual assistant. I will give you tasks and you will reply with the results of the tasks. I want you to only reply with the results of the tasks, and nothing else. Do not write explanations. My first task is ""I want you to write a short story about a cat"""',
|
73
|
+
accertivity: 1, # 1 is the most accertive, 2 is the least accertiv, this is called as temperature in openai
|
74
|
+
default_frequency: 0, # can be changed if you define a default prompt to when instance is initialized
|
75
|
+
default_presence: 0, # can be changed if you define a default prompt to when instance is initialized
|
76
|
+
has_placeholder: false, # has a placeholder in the prompt or role to be replaced by the user input
|
54
77
|
)
|
55
78
|
|
56
79
|
# Initialize a random actor
|
57
80
|
actor = AwesomeChatgptActors::Actor.new(
|
58
|
-
random: true
|
81
|
+
random: true,
|
59
82
|
)
|
60
83
|
|
84
|
+
|
85
|
+
# Initialize a random actor with a different language
|
86
|
+
actor = AwesomeChatgptActors::Actor.new(
|
87
|
+
random: true,
|
88
|
+
language: 'pt',
|
89
|
+
)
|
90
|
+
|
91
|
+
|
61
92
|
# Change the actor
|
62
|
-
actor.act_as(
|
93
|
+
actor.act_as('Virtual Assistant') # it will return the same actor with the new options, check the list in https://github.com/f/awesome-chatgpt-prompts/blob/main/prompts.csv
|
63
94
|
|
64
95
|
# Add a new actor to csv file
|
65
|
-
|
96
|
+
## The actor will be added to the csv file with the default options, the only required option is the actor role and prompt, if a different language is specified, it will add the prompt in the language specified csv file
|
97
|
+
AwesomeChatgptActors::CastControl.add_actor(actor: 'Virtual Assistant', prompt: 'I want you to act as a virtual assistant. I will give you tasks and you will reply with the results of the tasks. I want you to only reply with the results of the tasks, and nothing else. Do not write explanations. My first task is ""I want you to write a short story about a cat"""', has_placeholder: false, accertivity: 1, default_frequency: 0, default_presence: 0, language: 'en')
|
66
98
|
|
67
99
|
# Remove an actor from csv file
|
68
|
-
|
100
|
+
## The actor will be removed from the csv file, if a different language is specified, it will remove the actor from the language specified csv file
|
101
|
+
AwesomeChatgptActors::CastControl.remove_actor(actor: 'Virtual Assistant', language: 'en')
|
69
102
|
|
70
103
|
# Get a list of all actors
|
71
|
-
AwesomeChatgptActors::
|
104
|
+
AwesomeChatgptActors::CastControl.actors
|
105
|
+
|
106
|
+
# Get a list of all actors in a different language
|
107
|
+
AwesomeChatgptActors::CastControl.actors_pt
|
72
108
|
|
73
109
|
# Check if an actor exists
|
74
110
|
AwesomeChatgptActors::CastControl.actor_exists?(actor: 'Virtual Assistant')
|
75
111
|
|
112
|
+
# Check if an actor exists in a different language
|
113
|
+
AwesomeChatgptActors::CastControl.actor_exists_in_pt?(actor: 'Virtual Assistant')
|
114
|
+
|
76
115
|
# CSV file path
|
77
116
|
AwesomeChatgptActors::CastControl.csv_path # can be overriden by setting the environment variable CAST_CSV_PATH
|
78
117
|
|
118
|
+
# CSV file path in a different language
|
119
|
+
AwesomeChatgptActors::CastControl.pt_csv_path # can be overriden by setting the environment variable CAST_CSV_PATH_PT
|
120
|
+
|
79
121
|
# TEMP file path
|
80
122
|
AwesomeChatgptActors::CastControl.temp_path # can be overriden by setting the environment variable CAST_TEMP_PATH
|
81
123
|
```
|
82
124
|
|
83
125
|
## Testing
|
84
|
-
|
126
|
+
Create a .env_test file with the same variables as the .env file, but with the test values.
|
85
127
|
This gem was tested with Rspec. To run the tests, execute:
|
86
128
|
|
87
129
|
$ bundle exec rspec
|
88
130
|
|
89
131
|
<!-- Coverage: -->
|
90
132
|
<!-- local image path: coverage-image/simplecov-actors.png -->
|
133
|
+
[![Coverage Status](https://raw.githubusercontent.com/JesusGautamah/awesome-chatgpt-actors/master/coverage-image/simplecov-actors.png)]( https://raw.githubusercontent.com/JesusGautamah/awesome-chatgpt-actors/master/coverage-image/simplecov-actors.png)
|
91
134
|
|
92
135
|
## Contributing
|
93
136
|
|
@@ -15,7 +15,6 @@ Gem::Specification.new do |spec|
|
|
15
15
|
spec.required_ruby_version = ">= 2.6.0"
|
16
16
|
|
17
17
|
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
18
|
-
|
19
18
|
spec.metadata["homepage_uri"] = spec.homepage
|
20
19
|
spec.metadata["source_code_uri"] = spec.homepage
|
21
20
|
spec.metadata["changelog_uri"] = "#{spec.homepage}/releases"
|
@@ -28,6 +27,6 @@ Gem::Specification.new do |spec|
|
|
28
27
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
29
28
|
spec.require_paths = ["lib"]
|
30
29
|
|
31
|
-
spec.add_dependency "csv"
|
32
|
-
spec.
|
30
|
+
spec.add_dependency "csv", "~> 3.1"
|
31
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
33
32
|
end
|
@@ -3,33 +3,35 @@
|
|
3
3
|
module AwesomeChatgptActors
|
4
4
|
# This class is the implementation of Awesome Chatgpt Prompts with improvements
|
5
5
|
class Actor
|
6
|
-
def initialize(
|
7
|
-
|
6
|
+
def initialize(role: "Virtual Assistant", prompt: nil, language: "en", random: false,
|
7
|
+
has_placeholder: false, accertivity: 1, default_frequency: 0, default_presence: 0)
|
8
|
+
@csv_path = CastControl.csv_path if language == "en"
|
9
|
+
@csv_path = CastControl.csv_path.gsub("en", language) if language != "en"
|
8
10
|
@language = language || "en"
|
9
|
-
@openai_api_key = openai_api_key if openai_api_key
|
10
11
|
@actors_csv = CSV.read(@csv_path, headers: true)
|
11
|
-
@
|
12
|
-
|
12
|
+
@role = role
|
13
|
+
@prompt = prompt
|
14
|
+
@random = random
|
15
|
+
@accertivity = accertivity
|
16
|
+
@default_frequency = default_frequency
|
17
|
+
@default_presence = default_presence
|
18
|
+
@has_placeholder = has_placeholder
|
19
|
+
act_as(role) unless random || prompt
|
13
20
|
act_as(random_actor) if random
|
14
21
|
end
|
15
22
|
|
16
|
-
attr_accessor :
|
17
|
-
attr_reader :csv_path
|
18
|
-
|
19
|
-
def act_as(prompt_type = "Virtual Assistant")
|
20
|
-
@actor = prompt_type
|
21
|
-
actor_row = actors_csv.select { |row| row["act"] == actor }
|
22
|
-
raise ArgumentError, "Actor type not found: #{actor}" if actor_row.empty?
|
23
|
+
attr_accessor :role, :prompt, :actors_csv, :accertivity, :default_frequency, :default_presence, :has_placeholder
|
24
|
+
attr_reader :csv_path, :random
|
23
25
|
|
26
|
+
def act_as(role = "Virtual Assistant")
|
27
|
+
@role = role
|
28
|
+
actor_row = actors_csv.select { |row| row["act"] == role }
|
29
|
+
raise ArgumentError, "Role not found: #{role}" if actor_row.empty?
|
24
30
|
@prompt = actor_row.sample["prompt"]
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
raise "No actor initialized" if prompt.nil?
|
30
|
-
raise "No language provided" if lang.nil?
|
31
|
-
|
32
|
-
@prompt = translate_prompt(text: prompt, language: lang)
|
31
|
+
@accertivity = actor_row.sample["accertivity"].to_i
|
32
|
+
@default_frequency = actor_row.sample["default_frequency"].to_i
|
33
|
+
@default_presence = actor_row.sample["default_presence"].to_i
|
34
|
+
@has_placeholder = actor_row.sample["has_placeholder"] == "false" ? false : true
|
33
35
|
self
|
34
36
|
end
|
35
37
|
|
@@ -37,10 +39,6 @@ module AwesomeChatgptActors
|
|
37
39
|
|
38
40
|
attr_reader :language
|
39
41
|
|
40
|
-
def openai_api_key
|
41
|
-
@openai_api_key ||= ENV.fetch("OPENAI_API_KEY", nil)
|
42
|
-
end
|
43
|
-
|
44
42
|
private
|
45
43
|
|
46
44
|
def random_actor
|
@@ -49,17 +47,5 @@ module AwesomeChatgptActors
|
|
49
47
|
|
50
48
|
actor
|
51
49
|
end
|
52
|
-
|
53
|
-
def translate_prompt(text: nil, language: nil)
|
54
|
-
text = "Translate the following text from English to #{language}: \n\n#{text}\n\n"
|
55
|
-
client = OpenAI::Client.new(api_key: openai_api_key)
|
56
|
-
response = client.completions(
|
57
|
-
engine: "text-davinci-003", prompt: text,
|
58
|
-
temperature: 0.1, max_tokens: 200,
|
59
|
-
top_p: 1, frequency_penalty: 0,
|
60
|
-
presence_penalty: 0, stop: ["\n\n"]
|
61
|
-
)
|
62
|
-
response.choices.first.text
|
63
|
-
end
|
64
50
|
end
|
65
51
|
end
|
@@ -4,7 +4,11 @@ module AwesomeChatgptActors
|
|
4
4
|
# This class is responsible to control the actors csv file
|
5
5
|
class CastControl
|
6
6
|
def self.csv_path
|
7
|
-
ENV.fetch("CAST_CSV_NAME", "prompts-data/awesome-
|
7
|
+
ENV.fetch("CAST_CSV_NAME", "prompts-data/awesome-en-prompts.csv")
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.pt_csv_path
|
11
|
+
ENV.fetch("CAST_PT_CSV_NAME", "prompts-data/awesome-pt-prompts.csv")
|
8
12
|
end
|
9
13
|
|
10
14
|
def self.temp_path
|
@@ -17,17 +21,36 @@ module AwesomeChatgptActors
|
|
17
21
|
@actors_csv = CSV.read(csv_path, headers: true, quote_char: '"', col_sep: ",", row_sep: :auto)
|
18
22
|
end
|
19
23
|
|
24
|
+
def self.actors_pt_csv
|
25
|
+
raise "The csv file does not exist" unless File.exist?(pt_csv_path)
|
26
|
+
|
27
|
+
@actors_pt_csv = CSV.read(pt_csv_path, headers: true, quote_char: '"', col_sep: ",", row_sep: :auto)
|
28
|
+
end
|
29
|
+
|
20
30
|
def self.actors
|
21
31
|
raise "The csv file does not exist" unless File.exist?(csv_path)
|
22
32
|
|
23
33
|
actors_csv.map { |row| row["act"] }.uniq
|
24
34
|
end
|
25
35
|
|
26
|
-
def self.
|
36
|
+
def self.actors_pt
|
37
|
+
raise "The csv file does not exist" unless File.exist?(pt_csv_path)
|
38
|
+
|
39
|
+
actors_pt_csv.map { |row| row["act"] }.uniq
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.add_actor(actor: nil, prompt: nil, has_placeholder: false, accertivity: 1, default_frequency: 0, default_presence: 0, language: "en")
|
43
|
+
file_path = csv_path if language == "en"
|
44
|
+
file_path = pt_csv_path if language == "pt"
|
45
|
+
|
46
|
+
raise "The actor is nil" if actor.nil?
|
47
|
+
raise "The prompt is nil" if prompt.nil?
|
48
|
+
raise "File path is nil" if file_path.nil?
|
49
|
+
raise "The csv file does not exist" unless File.exist?(file_path)
|
27
50
|
raise "The actor already exists" if actor_exist?(actor: actor)
|
28
51
|
|
29
|
-
CSV.open(
|
30
|
-
csv << [actor, prompt]
|
52
|
+
CSV.open(file_path, "a", headers: true, quote_char: '"', col_sep: ",", force_quotes: true) do |csv|
|
53
|
+
csv << [actor, prompt, has_placeholder, accertivity, default_frequency, default_presence]
|
31
54
|
end
|
32
55
|
end
|
33
56
|
|
@@ -39,15 +62,32 @@ module AwesomeChatgptActors
|
|
39
62
|
act_column_to_array.include? actor
|
40
63
|
end
|
41
64
|
|
42
|
-
def self.
|
43
|
-
raise "The actor
|
65
|
+
def self.actor_exist_in_pt?(actor: nil)
|
66
|
+
raise "The actor is nil" if actor.nil?
|
67
|
+
raise "The csv file does not exist" unless File.exist?(pt_csv_path)
|
68
|
+
|
69
|
+
act_column_to_array = actors_pt_csv.map { |row| row["act"] }
|
70
|
+
act_column_to_array.include? actor
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.remove_actor(actor: nil, language: "en")
|
74
|
+
file_path = csv_path if language == "en"
|
75
|
+
file_path = pt_csv_path if language == "pt"
|
76
|
+
|
77
|
+
raise "The actor is nil" if actor.nil?
|
78
|
+
raise "File path is nil" if file_path.nil?
|
79
|
+
raise "The csv file does not exist" unless File.exist?(file_path)
|
80
|
+
(raise "The actor does not exist" unless actor_exist?(actor: actor)) if language == "en"
|
81
|
+
(raise "The actor does not exist" unless actor_exist_in_pt?(actor: actor)) if language == "pt"
|
44
82
|
|
45
|
-
actors_csv.delete_if { |row| row["act"] == actor }
|
83
|
+
actors_csv.delete_if { |row| row["act"] == actor } if language == "en"
|
84
|
+
actors_pt_csv.delete_if { |row| row["act"] == actor } if language == "pt"
|
46
85
|
CSV.open(temp_path, "w", headers: true, quote_char: '"', col_sep: ",", force_quotes: true) do |csv|
|
47
|
-
csv << %w[act prompt]
|
48
|
-
actors_csv.each { |row| csv << row unless row["act"] == actor }
|
86
|
+
csv << %w[act prompt has_placeholder accertivity default_frequency default_presence]
|
87
|
+
actors_csv.each { |row| csv << row unless row["act"] == actor } if language == "en"
|
88
|
+
actors_pt_csv.each { |row| csv << row unless row["act"] == actor } if language == "pt"
|
49
89
|
end
|
50
|
-
FileUtils.mv(temp_path,
|
90
|
+
FileUtils.mv(temp_path, file_path)
|
51
91
|
end
|
52
92
|
end
|
53
93
|
end
|
@@ -1,15 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
namespace :chatgpt_actors do
|
4
|
-
desc "Copy $GEM/prompts-data/awesome-
|
4
|
+
desc "Copy $GEM/prompts-data/awesome-language-prompts.csv to ./prompts-data/awesome-language-prompts.csv"
|
5
5
|
task :install do
|
6
|
-
|
7
|
-
.gem_dir + "/prompts-data/awesome-
|
8
|
-
|
9
|
-
|
6
|
+
en_path = Gem::Specification.find_by_name("awesome_chatgpt_actors")
|
7
|
+
.gem_dir + "/prompts-data/awesome-en-prompts.csv"
|
8
|
+
pt_path = Gem::Specification.find_by_name("awesome_chatgpt_actors")
|
9
|
+
.gem_dir + "/prompts-data/awesome-pt-prompts.csv"
|
10
|
+
paths = [en_path, pt_path]
|
11
|
+
raise "File not found: #{paths[0]}" unless File.exist?(paths[0])
|
12
|
+
raise "File not found: #{paths[1]}" unless File.exist?(paths[1])
|
10
13
|
|
11
14
|
FileUtils.mkdir_p("prompts-data") unless Dir.exist?("prompts-data")
|
12
|
-
|
13
|
-
|
15
|
+
|
16
|
+
FileUtils.cp(paths[0], "prompts-data/awesome-en-prompts.csv")
|
17
|
+
puts "Installed prompts-data/awesome-en-prompts.csv"
|
18
|
+
|
19
|
+
FileUtils.cp(paths[1], "prompts-data/awesome-pt-prompts.csv")
|
20
|
+
puts "Installed prompts-data/awesome-pt-prompts.csv"
|
21
|
+
puts "You can contribute to the project adding more actors and prompts or languages at"
|
22
|
+
puts "https://github.com/JesusGautamah/awesome_chatgpt_actors"
|
14
23
|
end
|
15
24
|
end
|