chatgpt-rb 0.1.0 → 0.1.2

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: 56bfd3ab0a5f56e05e8da9b5f59f7358383ebb1c78214d1afec32c6dd63fa140
4
- data.tar.gz: 3361ca43ea603a5634e95793cfff46215751a855e2836a92ba6161c4eb07d3fc
3
+ metadata.gz: 2dcefe52467b8bae3bc12c17e1f36a20b7a5a8a207f21c20d3db413b19f0a15e
4
+ data.tar.gz: 0cde2181779a22b431d8afd59dc89a80a1a5c58634fd02f7795cc31a9ee874be
5
5
  SHA512:
6
- metadata.gz: '09ab2d88f40d4b00eed86b3fbab7ab8fcc9b454f98c6f1ab0ac3b7461f97324f9151563540c9700d6e24d2ba47491ada4e611394453bd9b093ed2c026254f4cb'
7
- data.tar.gz: ff8ba450b24f98564a455c5994e8e19a3ad8cc0e85e4e3f14530a65a669e2a964ae8484815c14098fa6160c48e61b24a69f8cb46de4da4b5874d8a191881bb60
6
+ metadata.gz: 5ba3dd8cf78ef49ca4df7ed0521c309b8cc44bc837a0fd14aed5a7f346cced3a5204c56581514b19761d0f081b82edae25298a871eba7dfc409a226819f3a1b5
7
+ data.tar.gz: 73f97ff7857d9947cfd60036eb8c0b455e603fe73b84d02624ff693f969cfcdef47c8e078c3e7a95cb78fde9ca70fccffc3d2b52c36c9465220adfa4e3e8aa95
data/bin/chatgpt-rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "dotenv/load"
4
4
  require "colorize"
5
5
  require "reline"
6
+ require "optparse"
6
7
  require_relative "./../lib/chatgpt_rb"
7
8
 
8
9
  begin
@@ -10,17 +11,83 @@ begin
10
11
  rescue
11
12
  end
12
13
 
14
+ options = {
15
+ key: ENV["OPEN_AI_KEY"],
16
+ model: "gpt-3.5-turbo",
17
+ base_uri: "https://api.openai.com/v1",
18
+ functions_files: [],
19
+ }
20
+
21
+ OptionParser.new do |opts|
22
+ opts.banner = "Usage: chatgpt-rb [options]"
23
+
24
+ opts.on("-f", "--file FILE", "Load a previous conversation from FILE") do |file|
25
+ options[:file] = file
26
+ end
27
+
28
+ opts.on("-k", "--api-key KEY", "Use the provided API key for authentication") do |key|
29
+ options[:key] = key
30
+ end
31
+
32
+ opts.on("-m", "--model MODEL", "Use the provided MODEL (Default: #{options[:model]})") do |model|
33
+ options[:model] = model
34
+ end
35
+
36
+ opts.on("-b", "--base-uri URI", "Use the provided base URI (Default: #{options[:base_uri]})") do |uri|
37
+ options[:base_uri] = uri
38
+ end
39
+
40
+ opts.on("-u", "--functions-file FILE", "Add functions defined in FILE to your conversation") do |functions_file|
41
+ options[:functions_files] << functions_file
42
+ end
43
+
44
+ opts.on("-p", "--prompt PROMPT", "Declare the PROMPT for your conversation") do |prompt|
45
+ options[:prompt] = prompt
46
+ end
47
+ end.parse!
48
+
13
49
  begin
14
- conversation = ChatgptRb::Conversation.new(api_key: ENV.fetch("OPEN_AI_KEY"), model: ENV.fetch("OPEN_AI_MODEL", "gpt-3.5-turbo"))
50
+ puts "Type any message to talk with ChatGPT. Type '\\help' for a list of commands."
51
+
52
+ functions = options[:functions_files].map do |function_file|
53
+ puts "Loading functions from #{function_file}"
54
+
55
+ ChatgptRb::DSL::Conversation.new(ChatgptRb::Conversation.new).instance_eval(File.read(function_file))
56
+ end
57
+
58
+ messages = if options[:file]
59
+ JSON.parse(File.read(options[:file])).map { |hash| hash.transform_keys(&:to_sym) }
60
+ else
61
+ []
62
+ end
63
+
64
+ if options[:prompt]
65
+ puts "prompt> ".colorize(:blue) + options[:prompt]
66
+ end
15
67
 
16
- puts "Type any message to talk with ChatGPT. Type 'exit' to quit. Type 'dump' to dump this conversation to JSON."
68
+ conversation = ChatgptRb::Conversation.new(api_key: options.fetch(:key), model: options.fetch(:model), base_uri: options.fetch(:base_uri), messages:, functions:, prompt: options[:prompt])
17
69
 
18
70
  while message = Reline.readline("me> ".colorize(:red), true) do
19
71
  case message.chomp
20
- when "exit", "quit", "q", "\\q"
72
+ when "\\help", "\\h"
73
+ puts <<~COMMANDS.colorize(:blue)
74
+ - `\\quit` Exit
75
+ - `\\save <filename>` Save this conversation to a JSON file that can be reloaded later with the `-f` argument
76
+ - `\\functions` Get a list of configured functions
77
+ COMMANDS
78
+ when "\\q", "\\quit", "\\exit"
21
79
  exit
22
- when "dump"
80
+ when "\\dump"
23
81
  puts "dump> ".colorize(:blue) + conversation.messages.to_json
82
+ when "\\functions"
83
+ puts "available functions:".colorize(:blue)
84
+ functions.each do |function|
85
+ puts "- `#{function.name}` #{function.description}".colorize(:blue)
86
+ end
87
+ when /^\\save .+/
88
+ filename = /^\\save (.+)/.match(message)[1]
89
+ File.open(filename, "w") { |f| f.write(conversation.messages.to_json) }
90
+ puts "saved to #{filename} ".colorize(:blue)
24
91
  else
25
92
  print("ai> ".colorize(:yellow))
26
93
  conversation.ask(message) { |fragment| print(fragment) }
data/bin/watcher ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "listen"
4
+
5
+ directory_to_watch = Dir.pwd
6
+
7
+ listener = Listen.to(directory_to_watch) do |modified, added, removed|
8
+ system("rspec")
9
+ end
10
+
11
+ listener.start
12
+ puts "Watching #{directory_to_watch} for changes..."
13
+
14
+ # Stop the listener when the process is terminated
15
+ Signal.trap("INT") { listener.stop }
16
+ Signal.trap("TERM") { listener.stop }
17
+ sleep
@@ -1,12 +1,16 @@
1
1
  require "httparty"
2
+ require "json-schema"
3
+ require_relative "./function"
4
+ require_relative "./dsl/conversation"
2
5
 
3
6
  module ChatgptRb
4
7
  class Conversation
5
- attr_reader :api_key, :model, :functions, :temperature, :max_tokens, :top_p, :frequency_penalty, :presence_penalty, :messages
8
+ attr_accessor :api_key, :model, :functions, :temperature, :max_tokens, :top_p, :frequency_penalty, :presence_penalty, :prompt, :base_uri
9
+ attr_reader :messages
6
10
 
7
11
  # @param api_key [String]
8
12
  # @param model [String]
9
- # @param functions [Array<Hash>]
13
+ # @param functions [Array<Hash>, Array<ChatgptRb::Function>]
10
14
  # @param temperature [Float]
11
15
  # @param max_tokens [Integer]
12
16
  # @param top_p [Float]
@@ -14,63 +18,101 @@ module ChatgptRb
14
18
  # @param presence_penalty [Float]
15
19
  # @param messages [Array<Hash>]
16
20
  # @param prompt [String, nil] instructions that the model can use to inform its responses, for example: "Act like a sullen teenager."
17
- def initialize(api_key:, model: "gpt-3.5-turbo", functions: [], temperature: 0.7, max_tokens: 1024, top_p: 1.0, frequency_penalty: 0.0, presence_penalty: 0.0, messages: [], prompt: nil)
21
+ # @param base_uri [String]
22
+ def initialize(api_key: nil, model: "gpt-3.5-turbo", functions: [], temperature: 0.7, max_tokens: 1024, top_p: 1.0, frequency_penalty: 0.0, presence_penalty: 0.0, messages: [], prompt: nil, base_uri: "https://api.openai.com/v1", &configuration)
18
23
  @api_key = api_key
19
24
  @model = model
20
- @functions = functions
25
+ @functions = functions.each_with_object({}) do |function, hash|
26
+ func = function.is_a?(ChatgptRb::Function) ? function : ChatgptRb::Function.new(**function)
27
+ hash[func.name] = func
28
+ end
21
29
  @temperature = temperature
22
30
  @max_tokens = max_tokens
23
31
  @top_p = top_p
24
32
  @frequency_penalty = frequency_penalty
25
33
  @presence_penalty = presence_penalty
26
- @messages = messages
34
+ @messages = messages.map { |message| message.transform_keys(&:to_sym) }
35
+ @prompt = prompt
36
+ @base_uri = base_uri
37
+ ChatgptRb::DSL::Conversation.configure(self, &configuration) if block_given?
27
38
  @messages << { role: "system", content: prompt } if prompt
28
39
  end
29
40
 
30
41
  # @param content [String]
42
+ # @yieldparam [String] the response, but streamed
43
+ # @return [String] the response
31
44
  def ask(content, &block)
32
45
  @messages << { role: "user", content: }
33
46
  get_next_response(&block)
34
47
  end
35
48
 
49
+ # @param content [String]
50
+ # @param function [ChatgptRb::Function] temporarily enhance the next response with the provided function
51
+ # @yieldparam [String] the response, but streamed
52
+ # @return [String] the response
53
+ def ask_with_function(content, function, &block)
54
+ function_was = functions[function.name]
55
+ functions[function.name] = function
56
+ get_next_response(content, &block)
57
+ functions[function.name] = function_was
58
+ end
59
+
36
60
  private
37
61
 
38
62
  def <<(message)
39
63
  @messages << message
40
64
  end
41
65
 
66
+ # Ensure that each function's argument declarations conform to the JSON Schema
67
+ # See https://github.com/voxpupuli/json-schema/
68
+ def validate_functions!
69
+ metaschema = JSON::Validator.validator_for_name("draft4").metaschema
70
+ functions.values.each do |function|
71
+ raise ArgumentError, "Invalid function declaration for #{function.name}: #{function.as_json}" unless JSON::Validator.validate(metaschema, function.as_json)
72
+ end
73
+ end
74
+
42
75
  def get_next_response(&block)
76
+ validate_functions!
77
+
43
78
  streamed_content = ""
44
79
  streamed_arguments = ""
45
80
  streamed_role = ""
46
81
  streamed_function = ""
47
82
  error_buffer = []
48
83
 
84
+ body = {
85
+ model:,
86
+ messages: @messages,
87
+ temperature:,
88
+ max_tokens:,
89
+ top_p:,
90
+ frequency_penalty:,
91
+ presence_penalty:,
92
+ stream: block_given?,
93
+ }.tap do |hash|
94
+ hash[:functions] = functions.values.map(&:as_json) unless functions.empty?
95
+ end
96
+
49
97
  response = HTTParty.post(
50
- "https://api.openai.com/v1/chat/completions",
98
+ "#{base_uri}/chat/completions",
51
99
  steam_body: block_given?,
52
100
  headers: {
53
- "Content-Type": "application/json",
54
- "Authorization": "Bearer #{api_key}",
101
+ "Content-Type" => "application/json",
102
+ "Authorization" => "Bearer #{api_key}",
103
+ "Accept" => "application/json",
104
+ "User-Agent" => "Ruby/chatgpt-rb",
55
105
  },
56
- body: {
57
- model:,
58
- messages: @messages,
59
- temperature:,
60
- max_tokens:,
61
- top_p:,
62
- frequency_penalty:,
63
- presence_penalty:,
64
- stream: block_given?,
65
- }.tap { |hash| hash[:functions] = functions.map { |hash| hash.except(:implementation) } unless functions.empty? }.to_json,
106
+ body: body.to_json,
66
107
  ) do |fragment|
67
108
  if block_given?
68
109
  fragment.each_line do |line|
69
- next if line.nil?
70
- next if line == "\n"
71
110
  break if line == "data: [DONE]\n"
72
111
 
73
- line_without_prefix = line.gsub(/^data: /, "")
112
+ line_without_prefix = line.gsub(/^data: /, "").rstrip
113
+
114
+ next if line_without_prefix.empty?
115
+
74
116
  json = JSON.parse(line_without_prefix)
75
117
 
76
118
  break if json.dig("choices", 0, "finish_reason")
@@ -100,26 +142,25 @@ module ChatgptRb
100
142
  error_buffer.each { |e| $stderr.puts("Error: #{e}") }
101
143
 
102
144
  @messages << if block_given? && streamed_content != ""
103
- { "content" => streamed_content, "role" => streamed_role }
145
+ { content: streamed_content, role: streamed_role }
104
146
  elsif block_given? && streamed_arguments != ""
105
- { "role" => "assistant", "content" => nil, "function_call" => { "name" => streamed_function, "arguments" => streamed_arguments } }
147
+ { role: "assistant", content: nil, function_call: { "name" => streamed_function, "arguments" => streamed_arguments } }
106
148
  else
107
- response.dig("choices", 0, "message")
149
+ response.dig("choices", 0, "message").transform_keys(&:to_sym)
108
150
  end
109
151
 
110
- if @messages.last["content"]
111
- @messages.last["content"]
112
- elsif @messages.last["function_call"]
113
- function_args = @messages.last["function_call"]
152
+ if @messages.last[:content]
153
+ @messages.last[:content]
154
+ elsif @messages.last[:function_call]
155
+ function_args = @messages.last[:function_call]
114
156
  function_name = function_args.fetch("name")
115
157
  arguments = JSON.parse(function_args.fetch("arguments"))
116
-
117
- function = functions.find { |function| function[:name] == function_name }
118
- content = function.fetch(:implementation).call(**arguments.transform_keys(&:to_sym))
158
+ function = functions[function_name]
159
+ content = function.implementation.call(**arguments.transform_keys(&:to_sym))
119
160
 
120
161
  @messages << { role: "function", name: function_name, content: content.to_json }
121
162
 
122
- get_next_response(functions:, api_key:, model:, temperature:, max_tokens:, top_p:, frequency_penalty:, presence_penalty:, &block)
163
+ get_next_response(&block)
123
164
  end
124
165
  end
125
166
  end
@@ -0,0 +1,29 @@
1
+ module ChatgptRb
2
+ module DSL
3
+ class Base
4
+ # @param object [Chatgpt::Conversation, ChatgptRb::Function, ChatgptRb::Parameter]
5
+ # @param configuration [Block]
6
+ # @return [Chatgpt::Conversation, ChatgptRb::Function, ChatgptRb::Parameter]
7
+ def self.configure(object, &configuration)
8
+ new(object).instance_eval(&configuration)
9
+ object
10
+ end
11
+
12
+ attr_reader :object
13
+
14
+ # @param object [Chatgpt::Conversation, ChatgptRb::Function, ChatgptRb::Parameter]
15
+ def initialize(object)
16
+ @object = object
17
+ end
18
+
19
+ # @param [Array<Symbol>] shorthand for allowing the DSL to set iVars
20
+ def self.supported_fields(fields)
21
+ fields.each do |method_name|
22
+ define_method method_name do |value|
23
+ object.public_send("#{method_name}=", value)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,17 @@
1
+ require_relative "./base"
2
+ require_relative "./function"
3
+ require_relative "../function"
4
+
5
+ module ChatgptRb
6
+ module DSL
7
+ class Conversation < Base
8
+ supported_fields %i[api_key model functions temperature max_tokens top_p frequency_penalty presence_penalty prompt]
9
+
10
+ # @param name [String] the name of the function
11
+ # @param configuration [Block]
12
+ def function(name, &configuration)
13
+ object.functions[name] = ChatgptRb::DSL::Function.configure(ChatgptRb::Function.new(name:), &configuration)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ require_relative "../parameter"
2
+ require_relative "./parameter"
3
+
4
+ module ChatgptRb
5
+ module DSL
6
+ class Function < Base
7
+ supported_fields %i[description implementation parameters]
8
+
9
+ def parameter(name, &configuration)
10
+ object.parameters << ChatgptRb::DSL::Parameter.configure(ChatgptRb::Parameter.new(name:), &configuration)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ require_relative "./base"
2
+
3
+ module ChatgptRb
4
+ module DSL
5
+ class Parameter < Base
6
+ supported_fields %i[description type enum required]
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,31 @@
1
+ module ChatgptRb
2
+ class Function
3
+ attr_accessor :name, :description, :parameters, :implementation
4
+
5
+ # @param name [String, nil]
6
+ # @param description [String, nil]
7
+ # @param parameters [Array<ChatgptRb::Parameter>]
8
+ # @param implementation [Lambda, nil]
9
+ def initialize(name: nil, description: nil, parameters: [], implementation: nil)
10
+ @name = name
11
+ @description = description
12
+ @parameters = parameters
13
+ @implementation = implementation
14
+ end
15
+
16
+ # @return [Hash]
17
+ def as_json
18
+ {
19
+ name:,
20
+ description:,
21
+ parameters: {
22
+ type: "object",
23
+ properties: parameters.each_with_object({}) do |parameter, hash|
24
+ hash[parameter.name] = parameter.as_json
25
+ end,
26
+ },
27
+ required: parameters.select(&:required?).map(&:name),
28
+ }.compact
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ module ChatgptRb
2
+ class Parameter
3
+ attr_accessor :name, :enum, :type, :description, :required
4
+
5
+ # @param name [String]
6
+ # @param enum
7
+ # @param type
8
+ # @param description
9
+ # @param required [true, false] whether or not this parameter is required
10
+ def initialize(name:, enum: nil, type: nil, description: nil, required: false)
11
+ @name = name
12
+ @enum = enum
13
+ @type = type
14
+ @description = description
15
+ @required = required
16
+ end
17
+
18
+ def required?
19
+ !!required
20
+ end
21
+
22
+ # @return Hash
23
+ def as_json
24
+ {
25
+ enum:,
26
+ type:,
27
+ description:,
28
+ }.compact
29
+ end
30
+ end
31
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chatgpt-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Breckenridge
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-07 00:00:00.000000000 Z
11
+ date: 2023-07-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -66,6 +66,62 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: json-schema
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '4.0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '4.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: listen
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: webmock
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
69
125
  description: Provides libraries for interacting with the ChatGPT API and a CLI program
70
126
  `chatgpt-rb` for live conversations.
71
127
  email:
@@ -76,8 +132,15 @@ extensions: []
76
132
  extra_rdoc_files: []
77
133
  files:
78
134
  - bin/chatgpt-rb
135
+ - bin/watcher
79
136
  - lib/chatgpt_rb.rb
80
137
  - lib/chatgpt_rb/conversation.rb
138
+ - lib/chatgpt_rb/dsl/base.rb
139
+ - lib/chatgpt_rb/dsl/conversation.rb
140
+ - lib/chatgpt_rb/dsl/function.rb
141
+ - lib/chatgpt_rb/dsl/parameter.rb
142
+ - lib/chatgpt_rb/function.rb
143
+ - lib/chatgpt_rb/parameter.rb
81
144
  homepage: https://github.com/breckenedge/chatgpt-rb
82
145
  licenses:
83
146
  - MIT