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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 314d39333cd606e790f2258e93bcca1026a8b9a95b828ce635566874ad4d97ec
4
- data.tar.gz: 6b4b9a87a46f6cb84208728b25a510285f23667a2e1872457f8a9510bd017803
3
+ metadata.gz: 56d165955c63d1811168c341cc949fa01edef6afefdbee4fca59968f4d45bcc0
4
+ data.tar.gz: 035fe05f51a35bb5c94bcb2347bf4e3f783455d741d1922ae04e70928c0bd71d
5
5
  SHA512:
6
- metadata.gz: 14cc9e80b32707885731cc1d4f7dd3bb43e040fc7b0ec228e6ed0443547a2864c7b4c7492ba9d70f2ec464b5e8176fdceb4159a674c060a8906d2cc29c56ec2d
7
- data.tar.gz: a363accd2db5225c2aea3578bc2867dfa111455b5a75022f072de0bdf8a5de9bf4030ac95afd049607506ceedf1ddc91d531a8d82d15b3bac28eb41edcb4ef42
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 = complete(human_comment)
17
- p response if ENV["OPENAI_CHAT_DEBUG"]
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, "text").gsub(/\A\s+/, "") || ""
18
+ ai_comment = response.dig("choices", 0, "message", "content").gsub(/\A\s+/, "") || ""
21
19
 
22
- remember_dialog(Dialog.new(human_comment: human_comment, ai_comment: ai_comment, expire_at: expire_at))
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 ENV["OPENAI_CHAT_DEBUG"]
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 complete(human_comment)
35
+ def request_chat(human_comment)
35
36
  # https://beta.openai.com/examples/default-chat
36
- client.completions(
37
+ client.chat(
37
38
  parameters: {
38
- model: "text-davinci-003",
39
- temperature: 0.9,
40
- max_tokens: 512,
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
- def build_prompt(human_comment)
51
- prefix = [prompt_prefix]
52
- prefix += [ENV["OPENAI_CHAT_PRETEXT"]&.gsub(/\R/, " ")].compact
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
- dialogs = [example_dialog, *dialogs_from_memory,
55
- Dialog.new(human_comment: human_comment, ai_comment: "")].map do |dialog|
56
- dialog.to_prompt.chomp
57
- end.join("\n")
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
- <<~STRING.chomp
60
- #{prefix.join(" ")}
57
+ pre_messages = [*example_dialog, *messages_from_memory]
61
58
 
62
- #{dialogs}
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 dialogs_from_memory
68
- raw_dialogs.reject! { |hash| Dialog.from_hash(hash).expired? }
69
- raw_dialogs.map { |hash| Dialog.from_hash(hash) }
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 dialog [Dialog]
73
- def remember_dialog(dialog)
74
- raw_dialogs << dialog.to_h
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 raw_dialogs
79
- memory.namespace(NAMESPACE, message.from || "general")[:dialogs] ||= []
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") { 5 * 60 }.to_i
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
- Dialog.new(
100
- human_comment: "こんにちは。あなたは誰ですか?",
101
- ai_comment: "私は OpenAI 製の AI アシスタントの #{robot.name} です。なにかお手伝いできることはありますか?"
102
- )
94
+ [
95
+ Message.new(role: :user, content: "こんにちは。あなたは誰ですか?"),
96
+ Message.new(role: :assistant, content: "私は AI アシスタントの #{robot.name} です。なにかお手伝いできることはありますか?")
97
+ ]
103
98
  else
104
- Dialog.new(
105
- human_comment: "Hello, who are you?",
106
- ai_comment: "I'm #{robot.name}, an AI assistant created by OpenAI. How can I help you today?"
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ruboty
4
4
  module OpenAIChat
5
- VERSION = "0.1.2"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ruboty"
4
- require "ruby/openai"
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/dialog"
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
@@ -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", "~> 2.0"
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.1.2
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: 2022-12-20 00:00:00.000000000 Z
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: '2.0'
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: '2.0'
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/dialog.rb
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