ruboty-openai_chat 0.1.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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