chatgpt-rb 0.1.1 → 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: 50e30e865ade9a6e20cad5680617972114a224bda8bf25d9969b9b57e6236763
4
- data.tar.gz: e7d1714512f1696b9401018b11292a46ed220c650639521def9b4fa0290303a0
3
+ metadata.gz: 2dcefe52467b8bae3bc12c17e1f36a20b7a5a8a207f21c20d3db413b19f0a15e
4
+ data.tar.gz: 0cde2181779a22b431d8afd59dc89a80a1a5c58634fd02f7795cc31a9ee874be
5
5
  SHA512:
6
- metadata.gz: a42400a64dee74e93b7b27c8d04fc1f8c750bd0b90abc1ba37c33bdd58cbc9a494906c544b2bb0e02585807fffffbaae9ff3488d328b9bdb3051cc4fbfb59c0b
7
- data.tar.gz: 4f187489399450e128b1f5f1634004be8de0a5f4b71516bff12ec234e3cf5cf3498102d6e44081e0e2bd00dc8ad14c1f75ca68770425eac0a7b1e32d284ee6e4
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) }
@@ -1,14 +1,11 @@
1
1
  require "httparty"
2
+ require "json-schema"
2
3
  require_relative "./function"
3
4
  require_relative "./dsl/conversation"
4
5
 
5
6
  module ChatgptRb
6
7
  class Conversation
7
- include HTTParty
8
-
9
- base_uri "https://api.openai.com"
10
-
11
- attr_accessor :api_key, :model, :functions, :temperature, :max_tokens, :top_p, :frequency_penalty, :presence_penalty, :prompt
8
+ attr_accessor :api_key, :model, :functions, :temperature, :max_tokens, :top_p, :frequency_penalty, :presence_penalty, :prompt, :base_uri
12
9
  attr_reader :messages
13
10
 
14
11
  # @param api_key [String]
@@ -21,7 +18,8 @@ module ChatgptRb
21
18
  # @param presence_penalty [Float]
22
19
  # @param messages [Array<Hash>]
23
20
  # @param prompt [String, nil] instructions that the model can use to inform its responses, for example: "Act like a sullen teenager."
24
- 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, &configuration)
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)
25
23
  @api_key = api_key
26
24
  @model = model
27
25
  @functions = functions.each_with_object({}) do |function, hash|
@@ -33,8 +31,9 @@ module ChatgptRb
33
31
  @top_p = top_p
34
32
  @frequency_penalty = frequency_penalty
35
33
  @presence_penalty = presence_penalty
36
- @messages = messages
34
+ @messages = messages.map { |message| message.transform_keys(&:to_sym) }
37
35
  @prompt = prompt
36
+ @base_uri = base_uri
38
37
  ChatgptRb::DSL::Conversation.configure(self, &configuration) if block_given?
39
38
  @messages << { role: "system", content: prompt } if prompt
40
39
  end
@@ -64,7 +63,18 @@ module ChatgptRb
64
63
  @messages << message
65
64
  end
66
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
+
67
75
  def get_next_response(&block)
76
+ validate_functions!
77
+
68
78
  streamed_content = ""
69
79
  streamed_arguments = ""
70
80
  streamed_role = ""
@@ -84,8 +94,8 @@ module ChatgptRb
84
94
  hash[:functions] = functions.values.map(&:as_json) unless functions.empty?
85
95
  end
86
96
 
87
- response = self.class.post(
88
- "/v1/chat/completions",
97
+ response = HTTParty.post(
98
+ "#{base_uri}/chat/completions",
89
99
  steam_body: block_given?,
90
100
  headers: {
91
101
  "Content-Type" => "application/json",
@@ -97,11 +107,12 @@ module ChatgptRb
97
107
  ) do |fragment|
98
108
  if block_given?
99
109
  fragment.each_line do |line|
100
- next if line.nil?
101
- next if line == "\n"
102
110
  break if line == "data: [DONE]\n"
103
111
 
104
- line_without_prefix = line.gsub(/^data: /, "")
112
+ line_without_prefix = line.gsub(/^data: /, "").rstrip
113
+
114
+ next if line_without_prefix.empty?
115
+
105
116
  json = JSON.parse(line_without_prefix)
106
117
 
107
118
  break if json.dig("choices", 0, "finish_reason")
@@ -131,17 +142,17 @@ module ChatgptRb
131
142
  error_buffer.each { |e| $stderr.puts("Error: #{e}") }
132
143
 
133
144
  @messages << if block_given? && streamed_content != ""
134
- { "content" => streamed_content, "role" => streamed_role }
145
+ { content: streamed_content, role: streamed_role }
135
146
  elsif block_given? && streamed_arguments != ""
136
- { "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 } }
137
148
  else
138
- response.dig("choices", 0, "message")
149
+ response.dig("choices", 0, "message").transform_keys(&:to_sym)
139
150
  end
140
151
 
141
- if @messages.last["content"]
142
- @messages.last["content"]
143
- elsif @messages.last["function_call"]
144
- 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]
145
156
  function_name = function_args.fetch("name")
146
157
  arguments = JSON.parse(function_args.fetch("arguments"))
147
158
  function = functions[function_name]
@@ -2,11 +2,11 @@ module ChatgptRb
2
2
  class Function
3
3
  attr_accessor :name, :description, :parameters, :implementation
4
4
 
5
- # @param name [String]
5
+ # @param name [String, nil]
6
6
  # @param description [String, nil]
7
7
  # @param parameters [Array<ChatgptRb::Parameter>]
8
8
  # @param implementation [Lambda, nil]
9
- def initialize(name:, description: nil, parameters: [], implementation: nil)
9
+ def initialize(name: nil, description: nil, parameters: [], implementation: nil)
10
10
  @name = name
11
11
  @description = description
12
12
  @parameters = parameters
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.1
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-08 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,20 @@ 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'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: rspec
71
85
  requirement: !ruby/object:Gem::Requirement