ruboty-openai_chat 0.1.2 → 0.3.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 +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +7 -0
- data/lib/ruboty/handlers/openai_chat.rb +20 -0
- data/lib/ruboty/openai_chat/actions/base.rb +8 -0
- data/lib/ruboty/openai_chat/actions/chat.rb +45 -50
- data/lib/ruboty/openai_chat/actions/remember_profile.rb +23 -0
- data/lib/ruboty/openai_chat/actions/show_profile.rb +24 -0
- data/lib/ruboty/openai_chat/message.rb +47 -0
- data/lib/ruboty/openai_chat/version.rb +1 -1
- data/lib/ruboty/openai_chat.rb +9 -2
- data/ruboty-openai_chat.gemspec +1 -1
- metadata +7 -5
- data/lib/ruboty/openai_chat/dialog.rb +0 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 56d165955c63d1811168c341cc949fa01edef6afefdbee4fca59968f4d45bcc0
|
4
|
+
data.tar.gz: 035fe05f51a35bb5c94bcb2347bf4e3f783455d741d1922ae04e70928c0bd71d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8fd13b2d9393cc7971eb13555c98a1ff3c704e01d48cfbace79958f3b5adc3c18a8ecb1f1e3c928e2844a8f6ba22ec5d8dd69df98bafd5c5f39e9b32849b04b2
|
7
|
+
data.tar.gz: 272e9f664c55bf3d46542a06f1360018743a4a01228c92ab32cd27bb1af0157133a839e76912033ff494b500cd87ea262501941636754b5abff0b4ecf285b8a1
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,9 @@
|
|
1
1
|
## [Unreleased]
|
2
|
+
## [0.2.0] - 2022-12-26
|
3
|
+
|
4
|
+
- Add `remember openai profile` command to remember the pretext of AI prompt.
|
5
|
+
- Add `show openai profile` command to show the pretext of AI prompt.
|
6
|
+
- Make `OPENAI_CHAT_PRETEXT` optional.
|
2
7
|
|
3
8
|
## [0.1.0] - 2022-12-17
|
4
9
|
|
data/README.md
CHANGED
@@ -10,6 +10,13 @@ Install the gem and add to the application's Gemfile by executing:
|
|
10
10
|
|
11
11
|
$ bundle add ruboty-openai-chat
|
12
12
|
|
13
|
+
## Commands
|
14
|
+
|
15
|
+
```
|
16
|
+
ruboty /remember chatbot profile (?<body>.+)/ - Remembers given sentence as pretext of AI prompt
|
17
|
+
ruboty /show chatbot profile/ - Show the remembered profile
|
18
|
+
```
|
19
|
+
|
13
20
|
### ENV
|
14
21
|
|
15
22
|
- `OPENAI_ACCESS_TOKEN` - Pass OpenAI ACCESS TOKEN
|
@@ -18,9 +18,29 @@ module Ruboty
|
|
18
18
|
name: "chat"
|
19
19
|
)
|
20
20
|
|
21
|
+
on(
|
22
|
+
/remember chatbot profile (?<body>.+)/,
|
23
|
+
description: "Remembers given sentence as pretext of AI prompt",
|
24
|
+
name: "remember_profile"
|
25
|
+
)
|
26
|
+
|
27
|
+
on(
|
28
|
+
/show chatbot profile/,
|
29
|
+
description: "Show the remembered profile",
|
30
|
+
name: "show_profile"
|
31
|
+
)
|
32
|
+
|
21
33
|
def chat(message)
|
22
34
|
Ruboty::OpenAIChat::Actions::Chat.new(message).call
|
23
35
|
end
|
36
|
+
|
37
|
+
def remember_profile(message)
|
38
|
+
Ruboty::OpenAIChat::Actions::RememberProfile.new(message).call
|
39
|
+
end
|
40
|
+
|
41
|
+
def show_profile(message)
|
42
|
+
Ruboty::OpenAIChat::Actions::ShowProfile.new(message).call
|
43
|
+
end
|
24
44
|
end
|
25
45
|
end
|
26
46
|
end
|
@@ -5,6 +5,9 @@ module Ruboty
|
|
5
5
|
module Actions
|
6
6
|
# @abstract
|
7
7
|
class Base
|
8
|
+
NAMESPACE = "openai-chat-actions-chat"
|
9
|
+
PROFILE_KEY = "profile"
|
10
|
+
|
8
11
|
attr_reader :message
|
9
12
|
|
10
13
|
# @param message [Ruboty::Message]
|
@@ -27,6 +30,11 @@ module Ruboty
|
|
27
30
|
def memory
|
28
31
|
@memory ||= Memory.new(robot)
|
29
32
|
end
|
33
|
+
|
34
|
+
# @return [Array<String>]
|
35
|
+
def pretexts
|
36
|
+
[ENV["OPENAI_CHAT_PRETEXT"], memory.namespace(NAMESPACE)[PROFILE_KEY]].compact
|
37
|
+
end
|
30
38
|
end
|
31
39
|
end
|
32
40
|
end
|
@@ -6,77 +6,76 @@ module Ruboty
|
|
6
6
|
module OpenAIChat
|
7
7
|
module Actions
|
8
8
|
class Chat < Base
|
9
|
-
NAMESPACE = "openai-chat-actions-chat"
|
10
|
-
|
11
9
|
# @return [String]
|
12
10
|
attr_reader :human_comment, :ai_comment
|
13
11
|
|
14
12
|
def call
|
15
13
|
human_comment = message[:body]
|
16
|
-
response =
|
17
|
-
p response if
|
14
|
+
response = request_chat(human_comment)
|
15
|
+
p response if Ruboty::OpenAIChat.debug_mode?
|
18
16
|
raise response.body if response.code >= 400
|
19
17
|
|
20
|
-
ai_comment = response.dig("choices", 0, "
|
18
|
+
ai_comment = response.dig("choices", 0, "message", "content").gsub(/\A\s+/, "") || ""
|
21
19
|
|
22
|
-
|
20
|
+
remember_messages(
|
21
|
+
Message.new(role: :user, content: human_comment, expire_at: expire_at),
|
22
|
+
Message.new(role: :assistant, content: ai_comment, expire_at: expire_at)
|
23
|
+
)
|
23
24
|
message.reply(ai_comment)
|
24
25
|
rescue StandardError => e
|
25
26
|
forget
|
26
27
|
message.reply(e.message, code: true)
|
27
|
-
raise e if
|
28
|
+
raise e if Ruboty::OpenAIChat.debug_mode?
|
28
29
|
|
29
30
|
true
|
30
31
|
end
|
31
32
|
|
32
33
|
private
|
33
34
|
|
34
|
-
def
|
35
|
+
def request_chat(human_comment)
|
35
36
|
# https://beta.openai.com/examples/default-chat
|
36
|
-
client.
|
37
|
+
client.chat(
|
37
38
|
parameters: {
|
38
|
-
model: "
|
39
|
-
|
40
|
-
|
41
|
-
top_p: 1,
|
42
|
-
frequency_penalty: 0,
|
43
|
-
presence_penalty: 0.6,
|
44
|
-
stop: Dialog::STOP_SEQUENCES,
|
45
|
-
prompt: build_prompt(human_comment)
|
39
|
+
model: "gpt-3.5-turbo",
|
40
|
+
messages: build_messages(human_comment).map(&:to_api_hash),
|
41
|
+
temperature: 0.7
|
46
42
|
}
|
47
43
|
)
|
48
44
|
end
|
49
45
|
|
50
|
-
|
51
|
-
|
52
|
-
|
46
|
+
# @return [Array<Message>]
|
47
|
+
def build_messages(human_comment)
|
48
|
+
settings = <<~STRING.chomp
|
49
|
+
The following is a conversation with an AI assistant. The assistant is helpful, creative, clever, and very friendly. The AI assistant's name is #{robot.name}.
|
50
|
+
STRING
|
53
51
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
end
|
52
|
+
system_messages = [Message.new(role: :system, content: settings)]
|
53
|
+
pretexts.each do |pretext|
|
54
|
+
system_messages << Message.new(role: :system, content: pretext.chomp)
|
55
|
+
end
|
58
56
|
|
59
|
-
|
60
|
-
#{prefix.join(" ")}
|
57
|
+
pre_messages = [*example_dialog, *messages_from_memory]
|
61
58
|
|
62
|
-
|
63
|
-
STRING
|
59
|
+
[*system_messages, *pre_messages, Message.new(role: :user, content: human_comment)]
|
64
60
|
end
|
65
61
|
|
66
62
|
# @return [Array<Dialog>]
|
67
|
-
def
|
68
|
-
|
69
|
-
|
63
|
+
def messages_from_memory
|
64
|
+
current = Time.now
|
65
|
+
raw_messages.reject! { |hash| Message.from_hash(hash).expired?(current) }
|
66
|
+
raw_messages.map { |hash| Message.from_hash(hash) }
|
70
67
|
end
|
71
68
|
|
72
|
-
# @param
|
73
|
-
def
|
74
|
-
|
69
|
+
# @param messages [Array<Message>]
|
70
|
+
def remember_messages(*messages)
|
71
|
+
messages.each do |message|
|
72
|
+
raw_messages << message.to_h
|
73
|
+
end
|
75
74
|
end
|
76
75
|
|
77
76
|
# @return [Array<Hash>]
|
78
|
-
def
|
79
|
-
memory.namespace(NAMESPACE, message.from || "general")[:
|
77
|
+
def raw_messages
|
78
|
+
memory.namespace(NAMESPACE, message.from || "general")[:messages] ||= []
|
80
79
|
end
|
81
80
|
|
82
81
|
def forget
|
@@ -85,26 +84,22 @@ module Ruboty
|
|
85
84
|
|
86
85
|
# @return [Time]
|
87
86
|
def expire_at
|
88
|
-
Time.now + ENV.fetch("OPENAI_CHAT_MEMORIZE_SECONDS") {
|
89
|
-
end
|
90
|
-
|
91
|
-
# @return [String]
|
92
|
-
def prompt_prefix
|
93
|
-
"The following is a conversation with an AI assistant. The assistant is helpful, creative, clever, and very friendly. The AI assistant's name is #{robot.name}."
|
87
|
+
Time.now + ENV.fetch("OPENAI_CHAT_MEMORIZE_SECONDS") { 15 * 60 }.to_i
|
94
88
|
end
|
95
89
|
|
90
|
+
# @return [Array<Message>]
|
96
91
|
def example_dialog
|
97
92
|
case language
|
98
93
|
when :ja
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
94
|
+
[
|
95
|
+
Message.new(role: :user, content: "こんにちは。あなたは誰ですか?"),
|
96
|
+
Message.new(role: :assistant, content: "私は AI アシスタントの #{robot.name} です。なにかお手伝いできることはありますか?")
|
97
|
+
]
|
103
98
|
else
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
99
|
+
[
|
100
|
+
Message.new(role: :user, content: "Hello, who are you?"),
|
101
|
+
Message.new(role: :assistant, content: "I'm #{robot.name}, an AI assistant. How can I help you today?")
|
102
|
+
]
|
108
103
|
end
|
109
104
|
end
|
110
105
|
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Ruboty
|
6
|
+
module OpenAIChat
|
7
|
+
module Actions
|
8
|
+
class RememberProfile < Base
|
9
|
+
def call
|
10
|
+
new_profile = message[:body].strip
|
11
|
+
memory.namespace(NAMESPACE)[PROFILE_KEY] = new_profile
|
12
|
+
|
13
|
+
message.reply("Remembered the profile.")
|
14
|
+
rescue StandardError => e
|
15
|
+
message.reply(e.message, code: true)
|
16
|
+
raise e if ENV["OPENAI_CHAT_DEBUG"]
|
17
|
+
|
18
|
+
true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Ruboty
|
6
|
+
module OpenAIChat
|
7
|
+
module Actions
|
8
|
+
class ShowProfile < Base
|
9
|
+
def call
|
10
|
+
if (profile = memory.namespace(NAMESPACE)[PROFILE_KEY])
|
11
|
+
message.reply(profile, code: true)
|
12
|
+
else
|
13
|
+
message.reply("No profile is set.")
|
14
|
+
end
|
15
|
+
rescue StandardError => e
|
16
|
+
message.reply(e.message, code: true)
|
17
|
+
raise e if ENV["OPENAI_CHAT_DEBUG"]
|
18
|
+
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ruboty
|
4
|
+
module OpenAIChat
|
5
|
+
class Message
|
6
|
+
ROLES = %i[system user assistant].freeze
|
7
|
+
|
8
|
+
# @return [:system, :user, :assistant]
|
9
|
+
attr_reader :role
|
10
|
+
|
11
|
+
# @return [String]
|
12
|
+
attr_reader :content
|
13
|
+
|
14
|
+
# @return [Time]
|
15
|
+
attr_reader :expire_at
|
16
|
+
|
17
|
+
def self.from_hash(hash)
|
18
|
+
new(**hash.transform_keys(&:to_sym))
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param role [:system, :user, :assistant]
|
22
|
+
# @param content [String]
|
23
|
+
# @param expire_at [Time, Integer, nil]
|
24
|
+
def initialize(role:, content:, expire_at: nil)
|
25
|
+
@role = role.to_sym
|
26
|
+
raise ArgumentError, "role must be :system, :user, or :assistant" unless ROLES.include?(@role)
|
27
|
+
|
28
|
+
@content = content
|
29
|
+
@expire_at = expire_at&.yield_self { |t| Time.at(t) }
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [Hash]
|
33
|
+
def to_h
|
34
|
+
{ role: role, content: content, expire_at: expire_at&.to_i }
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_api_hash
|
38
|
+
{ role: role, content: content }
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Boolean]
|
42
|
+
def expired?(from = Time.now)
|
43
|
+
expire_at && (expire_at <= from)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/ruboty/openai_chat.rb
CHANGED
@@ -1,13 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "ruboty"
|
4
|
-
require "
|
4
|
+
require "openai"
|
5
5
|
|
6
6
|
require_relative "openai_chat/version"
|
7
7
|
require_relative "openai_chat/actions/base"
|
8
8
|
require_relative "openai_chat/actions/chat"
|
9
|
-
require_relative "openai_chat/
|
9
|
+
require_relative "openai_chat/actions/remember_profile"
|
10
|
+
require_relative "openai_chat/actions/show_profile"
|
10
11
|
require_relative "openai_chat/memory"
|
12
|
+
require_relative "openai_chat/message"
|
11
13
|
|
12
14
|
require_relative "handlers/openai_chat"
|
13
15
|
|
@@ -15,5 +17,10 @@ module Ruboty
|
|
15
17
|
module OpenAIChat
|
16
18
|
class Error < StandardError; end
|
17
19
|
# Your code goes here...
|
20
|
+
|
21
|
+
# @return [Boolean]
|
22
|
+
def self.debug_mode?
|
23
|
+
ENV["OPENAI_CHAT_DEBUG"]&.length&.positive?
|
24
|
+
end
|
18
25
|
end
|
19
26
|
end
|
data/ruboty-openai_chat.gemspec
CHANGED
@@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
|
|
31
31
|
# Uncomment to register a new dependency of your gem
|
32
32
|
# spec.add_dependency "example-gem", "~> 1.0"
|
33
33
|
spec.add_dependency "ruboty"
|
34
|
-
spec.add_dependency "ruby-openai", "~>
|
34
|
+
spec.add_dependency "ruby-openai", "~> 3.5"
|
35
35
|
|
36
36
|
# For more information and examples about making a new gem, check out our
|
37
37
|
# guide at: https://bundler.io/guides/creating_gem.html
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruboty-openai_chat
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tomoya Chiba
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-03-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ruboty
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '3.5'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '3.5'
|
41
41
|
description:
|
42
42
|
email:
|
43
43
|
- tomo.asleep@gmail.com
|
@@ -59,8 +59,10 @@ files:
|
|
59
59
|
- lib/ruboty/openai_chat.rb
|
60
60
|
- lib/ruboty/openai_chat/actions/base.rb
|
61
61
|
- lib/ruboty/openai_chat/actions/chat.rb
|
62
|
-
- lib/ruboty/openai_chat/
|
62
|
+
- lib/ruboty/openai_chat/actions/remember_profile.rb
|
63
|
+
- lib/ruboty/openai_chat/actions/show_profile.rb
|
63
64
|
- lib/ruboty/openai_chat/memory.rb
|
65
|
+
- lib/ruboty/openai_chat/message.rb
|
64
66
|
- lib/ruboty/openai_chat/version.rb
|
65
67
|
- ruboty-openai_chat.gemspec
|
66
68
|
homepage: https://github.com/tomoasleep/ruboty-openai_chat
|
@@ -1,53 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Ruboty
|
4
|
-
module OpenAIChat
|
5
|
-
class Dialog
|
6
|
-
STOP_SEQUENCES = [">> Human: ", ">> AI: "].freeze
|
7
|
-
STOP_SEQUENCE_PATTERN = />> (Human|AI): /.freeze
|
8
|
-
|
9
|
-
# @return [String]
|
10
|
-
attr_reader :human_comment, :ai_comment
|
11
|
-
|
12
|
-
# @return [Time]
|
13
|
-
attr_reader :expire_at
|
14
|
-
|
15
|
-
def self.from_hash(hash)
|
16
|
-
new(**hash.transform_keys(&:to_sym))
|
17
|
-
end
|
18
|
-
|
19
|
-
# @param human_comment [String]
|
20
|
-
# @param ai_comment [String]
|
21
|
-
# @param expire_at [Time, Integer, nil]
|
22
|
-
def initialize(human_comment:, ai_comment:, expire_at: nil)
|
23
|
-
@human_comment = human_comment
|
24
|
-
@ai_comment = ai_comment
|
25
|
-
@expire_at = expire_at&.yield_self { |t| Time.at(t) }
|
26
|
-
end
|
27
|
-
|
28
|
-
# @return [String]
|
29
|
-
def to_prompt
|
30
|
-
<<~STRING
|
31
|
-
>> Human: #{escape(human_comment).chomp}
|
32
|
-
>> AI: #{escape(ai_comment).chomp}
|
33
|
-
STRING
|
34
|
-
end
|
35
|
-
|
36
|
-
# @return [Hash]
|
37
|
-
def to_h
|
38
|
-
{ human_comment: human_comment, ai_comment: ai_comment, expire_at: expire_at&.to_i }
|
39
|
-
end
|
40
|
-
|
41
|
-
# @return [Boolean]
|
42
|
-
def expired?
|
43
|
-
expire_at && (expire_at <= Time.now)
|
44
|
-
end
|
45
|
-
|
46
|
-
private
|
47
|
-
|
48
|
-
def escape(text)
|
49
|
-
text.gsub(STOP_SEQUENCE_PATTERN, &:downcase)
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|