chatgpt-rb 0.1.0 → 0.1.2

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: 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