chatgpt-rb 0.1.6 → 0.1.7

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: 9c3a20f294a53407477460d0bcf1efc53524a4986cf1129c365f597a93766adc
4
- data.tar.gz: 75999a3494afb6b8726a1510c6e16ec29e8dc55015212175befef7f5fac3a766
3
+ metadata.gz: 1219489ab15f01f9161fc6c6ab35e5212c6851079559b7ae65303d7913e309af
4
+ data.tar.gz: 3c6951ea51ef8b71a25dc3c61ee5c6368f4c0f38bfc99e5f6d70782b43bf45e6
5
5
  SHA512:
6
- metadata.gz: 5a4aaa0890191fdd4c4b5221c49a3ce094e4d69f0ffd699207fa8987515fb4e12dd6ffcb349cd2015205e99ee65660b57c72f7c8c05e6a471703d557f50dbcaa
7
- data.tar.gz: 409b9eb640141bdd46ebbaf8b131c43b8f72d6e1e3b4da75c0f729ff9cf5bba1cb2b87a2536ddcf4fc906f6008f360a8694a5029ba57f43d55263f08783e10c6
6
+ metadata.gz: 29b1ce40f4cf7c5e88f710fed4eb7c47468fd92f5f053e2321bfa207de58c370ad67aaeac41f79b3264b50596cac48eb00d350fb81600756ff7562704b46374c
7
+ data.tar.gz: fef058d0addadd83568529d5b96544a4ead59dfac7878d200cdf506ac65f3c89f0e6ca32d70f0fe9d2a4eaff264131ffe30d7af18b17a293f3f2f10cf7e90f31
data/bin/chatgpt-rb CHANGED
@@ -14,6 +14,8 @@ options = {
14
14
  temperature: 0.7,
15
15
  }
16
16
 
17
+ SAVE_COMMAND_PATTERN = /sa?v?e? (.+)/.freeze
18
+
17
19
  OptionParser.new do |opts|
18
20
  opts.banner = "Usage: chatgpt-rb [options]"
19
21
 
@@ -74,32 +76,36 @@ begin
74
76
 
75
77
  commands = [
76
78
  {
77
- names: ["s", "save"],
79
+ names: ["s", "save", SAVE_COMMAND_PATTERN],
78
80
  description: "Save this conversation to a JSON file that can be reloaded later with the `-f` argument",
79
- implementation: ->() {
80
- filename = /^\\save (.+)/.match(message)[1]
81
- File.open(filename, "w") { |f| f.write(conversation.messages.to_json) }
82
- puts "saved to #{filename} ".colorize(:blue)
81
+ implementation: ->(message) {
82
+ filename = if message =~ SAVE_COMMAND_PATTERN
83
+ SAVE_COMMAND_PATTERN.match(message)[1]
84
+ else
85
+ "conversation_#{Time.now.iso8601}.json"
86
+ end
87
+ File.open(filename, "w") { |f| f.write(conversation.to_json) }
88
+ puts "saved to #{filename}".colorize(:blue)
83
89
  }
84
90
  },
85
91
  {
86
92
  names: ["q", "quit", "exit"],
87
93
  description: "Exit the program",
88
- implementation: ->() {
94
+ implementation: ->(_) {
89
95
  exit
90
96
  }
91
97
  },
92
98
  {
93
99
  names: ["d", "dump"],
94
100
  description: "Print out all messages in this converastion.",
95
- implementation: ->() {
101
+ implementation: ->(_) {
96
102
  puts "dump> ".colorize(:blue) + conversation.messages.to_json
97
103
  }
98
104
  },
99
105
  {
100
106
  names: ["f", "functions"],
101
107
  description: "List all available functions.",
102
- implementation: ->() {
108
+ implementation: ->(_) {
103
109
  puts "available functions:".colorize(:blue)
104
110
  functions.each do |function|
105
111
  puts "- `#{function.name}` #{function.description}".colorize(:blue)
@@ -109,7 +115,7 @@ begin
109
115
  {
110
116
  names: ["h", "help"],
111
117
  description: "List all commands and their description",
112
- implementation: ->() {
118
+ implementation: ->(_) {
113
119
  puts commands.map { |command| " - #{command[:names].map { |str| "`\\#{str}`".colorize(:yellow) }.join(", ")}: #{command[:description].colorize(:blue)}" }.join("\n")
114
120
  }
115
121
  }
@@ -117,8 +123,8 @@ begin
117
123
 
118
124
  while message = Reline.readline("me> ".colorize(:red), true) do
119
125
  input = message.chomp
120
- if (command = commands.find { |command| command[:names].any? { |name| "\\#{name}" == input } })
121
- command[:implementation].call
126
+ if (command = commands.find { |command| command[:names].any? { |name| name.is_a?(Regexp) ? input =~ name : "\\#{name}" == input } })
127
+ command[:implementation].call(message)
122
128
  else
123
129
  print("ai> ".colorize(:yellow))
124
130
  conversation.ask(message) { |fragment| print(fragment) }
@@ -1,3 +1,4 @@
1
+ require "json"
1
2
  require "httparty"
2
3
  require "json-schema"
3
4
  require_relative "./function"
@@ -5,7 +6,7 @@ require_relative "./dsl/conversation"
5
6
 
6
7
  module ChatgptRb
7
8
  class Conversation
8
- attr_accessor :api_key, :model, :functions, :temperature, :max_tokens, :top_p, :frequency_penalty, :presence_penalty, :prompt, :base_uri
9
+ attr_accessor :api_key, :model, :functions, :temperature, :max_tokens, :top_p, :frequency_penalty, :presence_penalty, :prompt, :base_uri, :seed, :json
9
10
  attr_reader :messages
10
11
 
11
12
  # @param api_key [String]
@@ -18,8 +19,10 @@ module ChatgptRb
18
19
  # @param presence_penalty [Float]
19
20
  # @param messages [Array<Hash>]
20
21
  # @param prompt [String, nil] instructions that the model can use to inform its responses, for example: "Act like a sullen teenager."
22
+ # @param json [true, false] whether or not ChatGPT should respond using only JSON objects
23
+ # @param seed [Integer, nil] deterministic best effort
21
24
  # @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
+ 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", json: false, seed: nil, &configuration)
23
26
  @api_key = api_key
24
27
  @model = model
25
28
  @functions = functions.each_with_object({}) do |function, hash|
@@ -42,10 +45,16 @@ module ChatgptRb
42
45
  @messages = messages.map { |message| message.transform_keys(&:to_sym) }
43
46
  @prompt = prompt
44
47
  @base_uri = base_uri
48
+ @json = json
49
+ @seed = seed
45
50
  ChatgptRb::DSL::Conversation.configure(self, &configuration) if block_given?
46
51
  @messages.unshift(role: "system", content: prompt) if prompt
47
52
  end
48
53
 
54
+ def to_json
55
+ messages.to_json
56
+ end
57
+
49
58
  # @param content [String]
50
59
  # @yieldparam [String] the response, but streamed
51
60
  # @return [String] the response
@@ -67,17 +76,10 @@ module ChatgptRb
67
76
 
68
77
  private
69
78
 
70
- def <<(message)
71
- @messages << message
72
- end
73
-
74
79
  # Ensure that each function's argument declarations conform to the JSON Schema
75
80
  # See https://github.com/voxpupuli/json-schema/
76
81
  def validate_functions!
77
- metaschema = JSON::Validator.validator_for_name("draft4").metaschema
78
- functions.values.each do |function|
79
- raise ArgumentError, "Invalid function declaration for #{function.name}: #{function.as_json}" unless JSON::Validator.validate(metaschema, function.as_json)
80
- end
82
+ functions.values.each(&:validate!)
81
83
  end
82
84
 
83
85
  def get_next_response(&block)
@@ -87,6 +89,7 @@ module ChatgptRb
87
89
  streamed_arguments = ""
88
90
  streamed_role = ""
89
91
  streamed_function = ""
92
+ streamed_tool_calls = ""
90
93
  error_buffer = []
91
94
 
92
95
  body = {
@@ -99,7 +102,9 @@ module ChatgptRb
99
102
  presence_penalty:,
100
103
  stream: block_given?,
101
104
  }.tap do |hash|
102
- hash[:functions] = functions.values.map(&:as_json) unless functions.empty?
105
+ hash[:tools] = functions.values.map(&:as_json) unless functions.empty?
106
+ hash[:response_format] = { type: :json_object } if json
107
+ hash[:seed] = seed unless seed.nil?
103
108
  end
104
109
 
105
110
  response = HTTParty.post(
@@ -140,6 +145,8 @@ module ChatgptRb
140
145
  streamed_content << content
141
146
  elsif arguments = json.dig("choices", 0, "delta", "function_call", "arguments")
142
147
  streamed_arguments << arguments
148
+ elsif tool_calls = json.dig("choices", 0, "delta", "tool_calls")
149
+ streamed_tool_calls << tool_calls
143
150
  end
144
151
  rescue => e
145
152
  error_buffer << "Error: #{e}"
@@ -155,12 +162,30 @@ module ChatgptRb
155
162
  { content: streamed_content, role: streamed_role }
156
163
  elsif block_given? && streamed_arguments != ""
157
164
  { role: "assistant", content: nil, function_call: { "name" => streamed_function, "arguments" => streamed_arguments } }
165
+ elsif block_given? && streamed_tool_calls != ""
166
+ { role: "assistant", content: nil, tool_calls: streamed_tool_calls }
158
167
  else
159
168
  response.dig("choices", 0, "message").transform_keys(&:to_sym)
160
169
  end
161
170
 
162
171
  if @messages.last[:content]
163
- @messages.last[:content]
172
+ json ? JSON.parse(@messages.last[:content]) : @messages.last[:content]
173
+ elsif @messages.last[:tool_calls]
174
+ @messages.last[:tool_calls].each do |tool_call|
175
+ next unless tool_call.fetch("type") == "function"
176
+ function_name = tool_call.dig("function", "name")
177
+ function_args = JSON.parse(tool_call.dig("function", "arguments"))
178
+ function = functions.fetch(function_name)
179
+ content = function.implementation.call(**function_args.transform_keys(&:to_sym))
180
+ @messages << {
181
+ role: "tool",
182
+ tool_call_id: tool_call.fetch("id"),
183
+ name: function_name,
184
+ content: content.to_json,
185
+ }
186
+ end
187
+
188
+ get_next_response(&block)
164
189
  elsif @messages.last[:function_call]
165
190
  function_args = @messages.last[:function_call]
166
191
  function_name = function_args.fetch("name")
@@ -5,7 +5,7 @@ require_relative "../function"
5
5
  module ChatgptRb
6
6
  module DSL
7
7
  class Conversation < Base
8
- supported_fields %i[api_key model functions temperature max_tokens top_p frequency_penalty presence_penalty prompt]
8
+ supported_fields %i[api_key model functions temperature max_tokens top_p frequency_penalty presence_penalty prompt json seed]
9
9
 
10
10
  # @param name [String] the name of the function
11
11
  # @param configuration [Block]
@@ -13,19 +13,29 @@ module ChatgptRb
13
13
  @implementation = implementation
14
14
  end
15
15
 
16
+ def validate!
17
+ metaschema = JSON::Validator.validator_for_name("draft4").metaschema
18
+ return if JSON::Validator.validate(metaschema, as_json[:function])
19
+
20
+ raise ArgumentError, "Invalid function declaration for #{name}: #{as_json[:function]}"
21
+ end
22
+
16
23
  # @return [Hash]
17
24
  def as_json
18
25
  {
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
- required: parameters.select(&:required?).map(&:name),
27
- },
28
- }.compact
26
+ type: "function",
27
+ function: {
28
+ name:,
29
+ description:,
30
+ parameters: {
31
+ type: "object",
32
+ properties: parameters.each_with_object({}) do |parameter, hash|
33
+ hash[parameter.name] = parameter.as_json
34
+ end,
35
+ required: parameters.select(&:required?).map(&:name),
36
+ },
37
+ }.compact
38
+ }
29
39
  end
30
40
  end
31
41
  end
@@ -1,3 +1,3 @@
1
1
  module ChatgptRb
2
- VERSION = "0.1.6"
2
+ VERSION = "0.1.7"
3
3
  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.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Breckenridge
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-05 00:00:00.000000000 Z
11
+ date: 2023-11-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -123,7 +123,8 @@ dependencies:
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
125
  description: Provides libraries for interacting with the ChatGPT API and a CLI program
126
- `chatgpt-rb` for live conversations. Supports writing functions in Ruby.
126
+ `chatgpt-rb` for live conversations. Supports writing tools (functions) in Ruby
127
+ and streaming responses.
127
128
  email:
128
129
  - aaron@breckridge.dev
129
130
  executables:
@@ -146,7 +147,7 @@ homepage: https://github.com/breckenedge/chatgpt-rb
146
147
  licenses:
147
148
  - MIT
148
149
  metadata: {}
149
- post_install_message:
150
+ post_install_message:
150
151
  rdoc_options: []
151
152
  require_paths:
152
153
  - lib
@@ -161,8 +162,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
161
162
  - !ruby/object:Gem::Version
162
163
  version: '0'
163
164
  requirements: []
164
- rubygems_version: 3.4.10
165
- signing_key:
165
+ rubygems_version: 3.4.21
166
+ signing_key:
166
167
  specification_version: 4
167
168
  summary: A gem for interacting with the ChatGPT API
168
169
  test_files: []