gpt-function 0.2.0 → 0.4.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: f08f33db34e0ff129539ed7a41b244d2866be076f5fc562ed327ff880a852014
4
- data.tar.gz: 2a9913da1af1d6ab5ea10602aa5c073c113b6d45d6cdd761eedb4399b32615fc
3
+ metadata.gz: cdeb301d12f6247b694accec4654d4d65bab0ca071cb4bf7ae73631bf540bc56
4
+ data.tar.gz: dd5cb067931ac6411a545641b96f40ef81d929bd1d388cfe93808d5c76b6e2ec
5
5
  SHA512:
6
- metadata.gz: 79b7276238753eb6613c7896a796b1501dd6b3059af1e9d1b8d4c0f01cc42a4fe64210833f5f088846aa2b098d1f516c906b62056e0ff739d6cea2f87874e7a3
7
- data.tar.gz: 6590b337edb6edc2215ad3a6192363dfe852c0735c3af75c4fd0662c7ab2219221a7e49be88037968d858c2d55ba5f72a37b259e63b00a703df7498320c77f91
6
+ metadata.gz: 665474c59064f7ae3401b0fea4e8ca562cfa2e91ff285523bbcdff9b367480f2d137b619b15d30c396df92a07c506bd73ed121bd8c54292d301785957486aa37
7
+ data.tar.gz: f1072362a33e579c4f52eda7b8abbadc3970807eaca91c5aa39010ca130c3e935a9ee5194b207a8c4de8b11c2079a68bbc3675d63ddeb74d811f9c2de7c33aac
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gpt-function (0.1.2)
4
+ gpt-function (0.4.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -13,6 +13,7 @@ GEM
13
13
  crack (0.4.5)
14
14
  rexml
15
15
  diff-lcs (1.5.0)
16
+ dotenv (3.1.2)
16
17
  hashdiff (1.0.1)
17
18
  json (2.6.3)
18
19
  language_server-protocol (3.17.0.3)
@@ -64,6 +65,7 @@ PLATFORMS
64
65
 
65
66
  DEPENDENCIES
66
67
  byebug (~> 11.1)
68
+ dotenv
67
69
  gpt-function!
68
70
  rake (~> 13.0)
69
71
  rspec (~> 3.0)
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  這個套件支援你在 Ruby 程式中使用 GPT 函數。
4
4
 
5
- 你可以確保每次呼叫 GPT 函數時,都會得到相同的結果。
5
+ 你可以確保每次呼叫 GPT 函數時,接近 100% 都會得到相同的結果。
6
6
 
7
7
  目前能夠使用的模型有:
8
8
 
@@ -12,7 +12,13 @@
12
12
 
13
13
  ## Installation
14
14
 
15
- ...
15
+ 在你的 Gemfile 中加入下面這行:
16
+
17
+ ```ruby
18
+ gem 'gpt-function'
19
+ ```
20
+
21
+ 就可以使用 `bundle install` 安裝這個套件。
16
22
 
17
23
  ## Usage
18
24
 
@@ -21,29 +27,92 @@
21
27
  require 'gpt-function'
22
28
 
23
29
  # 你需要設定你的 api key 和 model name
24
- Gpt::Function.configure(api_key: '...', model: 'gpt-3.5-turbo-1106')
25
-
26
- # 創建一個簡單的 GPT 函數,你需要描述這個函數的功能,以及提供一些範例
27
- translater = Gpt::Function.new("請翻譯成繁體中文", [["apple", "蘋果"]])
30
+ GptFunction.configure(api_key: '...', model: 'gpt-4o-mini', batch_storage: MyBatchStorage)
28
31
 
29
- # 然後就可以使用這個函數了
30
- result = translater.call("apple")
32
+ # 使用內建的翻譯方法
33
+ p GptFunctions.翻譯成中文.call("banana") # "香蕉"
31
34
 
32
- # 回傳的結果型別會參考範例的型別
33
- puts result # "蘋果"
35
+ # 使用內建的擷取關鍵字方法
36
+ p GptFunctions.擷取關鍵字.call("臺北市政府推動綠色交通計劃,鼓勵民眾使用公共運輸和自行車") # ["臺北市政府", "綠色交通計劃", "民眾", "公共運輸", "自行車"]
34
37
 
35
- # 一個較複雜的 GPT 函數,用於擷取關鍵字
36
- keywords_extractor = Gpt::Function.new("請擷取出所有關鍵字", [
38
+ # 你也可以自己定義方法
39
+ def 擷取關鍵字
40
+ # 創建一個簡單的 GPT 函數,你需要描述這個函數的功能,以及提供一些範例
41
+ GptFunction.new("Extract all keywords",
42
+ [
37
43
  [
38
- "藍白合「難產」!游盈隆認定「平手」 稱解套辦法就是「再做一次民調」",
39
- ["藍白合", "難產", "游盈隆", "解套", "民調"]
44
+ "臺灣最新5G網路覆蓋率達95%,推動智慧城市發展,領先亞洲多國",
45
+ ["臺灣", "5G網路", "覆蓋率", "智慧城市", "亞洲"]
40
46
  ]
41
- ]
42
- )
43
- result = keywords_extractor.call("藍白「3%各表」翻臉倒數?學者曝1關鍵指標")
47
+ ])
48
+ end
49
+ ```
50
+
51
+ Batch Storage 是一個用來儲存 GPT 函數的結果的類別,你可以自己定義一個類似的類別,並且在 `GptFunction.configure` 中設定。
52
+
53
+ ```ruby
54
+ class MyBatchStorage
55
+ def initialize
56
+ @queue = []
57
+ end
58
+
59
+ def enqueue(value)
60
+ @queue << value
61
+ true
62
+ end
63
+
64
+ def dequeue
65
+ @queue.shift
66
+ end
67
+ end
68
+
69
+ GptFunction.configure(api_key: '...', model: 'gpt-4o-mini', batch_storage: MyBatchStorage)
70
+ ```
71
+
72
+ 你可以用 Batch.create 建立一個新的 Batch, 在 create 成功時,會自動將 Batch 存入 BatchStorage 中。
73
+
74
+ ```ruby
75
+ request1 = GptFunctions.翻譯成中文.to_request_body("apple")
76
+ request2 = GptFunctions.翻譯成中文.to_request_body("tesla")
77
+ batch = GptFunction::Batch.create([request1, request2])
78
+ ```
79
+
80
+ 你可以用 Batch.process 來處理 Batch,如果 Batch 的 status 在 "failed", "completed", "expired", "cancelled" 當中,Batch 會被從 queue 中移除,如果是其他狀態,Batch 會自動重新加入 queue 中,你只需要定期持續呼叫 process 就可以。
81
+
82
+ ```ruby
83
+ GptFunction::Batch.process do |batch|
84
+ puts "batch id: #{batch.id}, status: #{batch.status}, progress: #{batch.request_counts_completed}/#{batch.request_counts_total}"
85
+ batch.pairs.each do |input, output|
86
+ puts "input: #{input}, output: #{output}"
87
+ end
88
+ end
89
+ ```
44
90
 
45
- # 可以看到回傳的型別是陣列
46
- puts result # ["藍白", "3%各表", "翻臉", "關鍵指標"]
91
+ 可以用 count 參數來限制每次處理的數量,預設值為 1。
92
+
93
+ ```ruby
94
+ GptFunction::Batch.process(count: 2) do |batch|
95
+ ...
96
+ end
97
+ ```
98
+
99
+ Batch Storage 整合 Active Record 的範例:
100
+
101
+ ```ruby
102
+ class Model < ApplicationRecord
103
+ class << self
104
+ def enqueue(hash)
105
+ create!(hash)
106
+ true
107
+ end
108
+
109
+ def dequeue
110
+ first&.destroy
111
+ end
112
+ end
113
+ end
114
+
115
+ GptFunction.configure(api_key: '...', model: 'gpt-4o-mini', batch_storage: Model)
47
116
  ```
48
117
 
49
118
  ## License
data/gpt-function.gemspec CHANGED
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "lib/gpt/function/version"
3
+ require_relative "lib/gpt_function/version"
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "gpt-function"
7
- spec.version = Gpt::Function::VERSION
7
+ spec.version = GptFunction::VERSION
8
8
  spec.authors = ["etrex kuo"]
9
9
  spec.email = ["et284vu065k3@gmail.com"]
10
10
 
@@ -32,6 +32,8 @@ Gem::Specification.new do |spec|
32
32
  # Uncomment to register a new dependency of your gem
33
33
  # spec.add_dependency "example-gem", "~> 1.0"
34
34
 
35
+ spec.add_development_dependency "dotenv"
36
+
35
37
  # For more information and examples about making a new gem, check out our
36
38
  # guide at: https://bundler.io/guides/creating_gem.html
37
39
  end
data/lib/gpt-function.rb CHANGED
@@ -1,4 +1,125 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "gpt/function"
4
- require_relative "gpt/functions"
3
+ require "net/http"
4
+ require "json"
5
+
6
+ require_relative "gpt_function/version"
7
+ require_relative "gpt_function/file"
8
+ require_relative "gpt_function/storage"
9
+ require_relative "gpt_function/simple_queue"
10
+ require_relative "gpt_function/batch"
11
+ require_relative "gpt_functions"
12
+
13
+ class GptFunction
14
+ class Error < StandardError; end
15
+
16
+ @api_key = nil
17
+ @model = nil
18
+
19
+ class << self
20
+ attr_accessor :api_key, :model
21
+
22
+ def configure(api_key:, model:, batch_storage: GptFunction::SimpleQueue.new)
23
+ @api_key = api_key
24
+ @model = model
25
+ GptFunction::Storage.batch_storage = batch_storage
26
+ end
27
+ end
28
+
29
+ def initialize(prompt, examples = [], temperature = 0)
30
+ @temperature = temperature
31
+ @messages = [
32
+ {
33
+ role: "system",
34
+ content: "#{prompt}\n Note: The response format is always a JSON with the key output like this:{output: ...}"
35
+ },
36
+ *examples.flat_map do |example|
37
+ [
38
+ {
39
+ role: "user",
40
+ content: example[0]
41
+ },
42
+ {
43
+ role: "assistant",
44
+ content: { output: example[1] }.to_json
45
+ }
46
+ ]
47
+ end
48
+ ]
49
+ end
50
+
51
+ def call(input)
52
+ # 使用類別級別的變量來發送請求
53
+ response = send_request(input)
54
+ body = response.body.force_encoding("UTF-8")
55
+ json = JSON.parse(body)
56
+ # 處理可能的錯誤回應
57
+ raise StandardError, json.dig("error", "message") if json.dig("error", "code")
58
+
59
+ # 處理正常的回應
60
+ JSON.parse(json.dig("choices", 0, "message", "content"))["output"]
61
+ end
62
+
63
+ def to_request_body(input)
64
+ {
65
+ model: GptFunction.model,
66
+ response_format: {
67
+ type: "json_object"
68
+ },
69
+ seed: 42,
70
+ messages: [
71
+ *@messages,
72
+ {
73
+ "role": "user",
74
+ "content": input
75
+ }
76
+ ],
77
+ temperature: @temperature
78
+ }
79
+ end
80
+
81
+ def batch(inputs, post_processor_class)
82
+ file_content = inputs.map.with_index do |input, index|
83
+ {
84
+ "custom_id": "request-#{index + 1}",
85
+ "method": "POST",
86
+ "url": "/v1/chat/completions",
87
+ "body": to_request_body(input)
88
+ }
89
+ end
90
+
91
+ batch_instance = Batch.new(GptFunction.api_key)
92
+ batch_id = batch_instance.request(file_content)
93
+ puts "Batch created with ID: #{batch_id}"
94
+
95
+ # 創建 BatchRequest 並啟動 ProcessBatchJob
96
+ batch_request = BatchRequest.create(
97
+ batch_id: batch_id,
98
+ status: 'created',
99
+ total_request_counts: inputs.size,
100
+ completed_request_counts: 0,
101
+ post_processor_class: post_processor_class.to_s
102
+ )
103
+ ProcessBatchJob.perform_later(batch_request.id)
104
+ end
105
+
106
+ private
107
+
108
+ def send_request(input)
109
+ uri = URI.parse("https://api.openai.com/v1/chat/completions")
110
+ request = Net::HTTP::Post.new(uri)
111
+ request.content_type = "application/json"
112
+ request["Authorization"] = "Bearer #{GptFunction.api_key}"
113
+ request.body = to_request_body(input).to_json
114
+
115
+ req_options = {
116
+ use_ssl: uri.scheme == "https",
117
+ open_timeout: 60, # opening a connection timeout
118
+ read_timeout: 300 # reading one block of response timeout
119
+ }
120
+
121
+ Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
122
+ http.request(request)
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,298 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+
6
+ class GptFunction
7
+ class Batch
8
+ attr_reader :id
9
+ attr_reader :object
10
+ attr_reader :endpoint
11
+ attr_reader :errors
12
+ attr_reader :input_file_id
13
+ attr_reader :completion_window
14
+ attr_reader :status
15
+ attr_reader :output_file_id
16
+ attr_reader :error_file_id
17
+ attr_reader :created_at
18
+ attr_reader :in_progress_at
19
+ attr_reader :expires_at
20
+ attr_reader :finalizing_at
21
+ attr_reader :completed_at
22
+ attr_reader :failed_at
23
+ attr_reader :expired_at
24
+ attr_reader :cancelling_at
25
+ attr_reader :cancelled_at
26
+
27
+ attr_reader :request_counts_total
28
+ attr_reader :request_counts_completed
29
+ attr_reader :request_counts_failed
30
+
31
+ attr_reader :metadata_customer_id
32
+ attr_reader :metadata_batch_description
33
+
34
+ def initialize(hash)
35
+ @id = hash["id"]
36
+ @object = hash["object"]
37
+ @endpoint = hash["endpoint"]
38
+ @errors = hash["errors"]
39
+ @input_file_id = hash["input_file_id"]
40
+ @completion_window = hash["completion_window"]
41
+ @status = hash["status"]
42
+ @output_file_id = hash["output_file_id"]
43
+ @error_file_id = hash["error_file_id"]
44
+ @created_at = hash["created_at"]
45
+ @in_progress_at = hash["in_progress_at"]
46
+ @expires_at = hash["expires_at"]
47
+ @finalizing_at = hash["finalizing_at"]
48
+ @completed_at = hash["completed_at"]
49
+ @failed_at = hash["failed_at"]
50
+ @expired_at = hash["expired_at"]
51
+ @cancelling_at = hash["cancelling_at"]
52
+ @cancelled_at = hash["cancelled_at"]
53
+
54
+ @request_counts_total = hash.dig("request_counts", "total")
55
+ @request_counts_completed = hash.dig("request_counts", "completed")
56
+ @request_counts_failed = hash.dig("request_counts", "failed")
57
+
58
+ @metadata_customer_id = hash.dig("metadata", "customer_id")
59
+ @metadata_batch_description = hash.dig("metadata", "batch_description")
60
+ end
61
+
62
+ def to_hash
63
+ {
64
+ id: id,
65
+ object: object,
66
+ endpoint: endpoint,
67
+ errors: errors,
68
+ input_file_id: input_file_id,
69
+ completion_window: completion_window,
70
+ status: status,
71
+ output_file_id: output_file_id,
72
+ error_file_id: error_file_id,
73
+ created_at: created_at,
74
+ in_progress_at: in_progress_at,
75
+ expires_at: expires_at,
76
+ finalizing_at: finalizing_at,
77
+ completed_at: completed_at,
78
+ failed_at: failed_at,
79
+ expired_at: expired_at,
80
+ cancelling_at: cancelling_at,
81
+ cancelled_at: cancelled_at,
82
+ request_counts_total: request_counts_total,
83
+ request_counts_completed: request_counts_completed,
84
+ request_counts_failed: request_counts_failed,
85
+ metadata_customer_id: metadata_customer_id,
86
+ metadata_batch_description: metadata_batch_description,
87
+ }
88
+ end
89
+
90
+ def to_s
91
+ to_hash.to_json
92
+ end
93
+
94
+ def inspect
95
+ to_hash.to_json
96
+ end
97
+
98
+ def input_file
99
+ return nil if input_file_id.nil?
100
+ @input_file ||= File.from_id(input_file_id)
101
+ end
102
+
103
+ def output_file
104
+ return nil if output_file_id.nil?
105
+ @output_file ||= File.from_id(output_file_id)
106
+ end
107
+
108
+ def input_jsonl
109
+ @input_jsonl ||= input_file&.jsonl || []
110
+ end
111
+
112
+ def output_jsonl
113
+ @output_jsonl ||= output_file&.jsonl || []
114
+ end
115
+
116
+ def inputs
117
+ @inputs ||= input_jsonl.map do |hash|
118
+ {
119
+ "custom_id" => hash.dig("custom_id"),
120
+ "content" => hash.dig("body", "messages", -1, "content")
121
+ }
122
+ end
123
+ end
124
+
125
+ def outputs
126
+ @outputs ||= output_jsonl.map do |hash|
127
+ content = hash.dig("response", "body", "choices", 0, "message", "content")
128
+ content = JSON.parse(content)["output"] rescue content
129
+ {
130
+ "custom_id" => hash.dig("custom_id"),
131
+ "content" => content
132
+ }
133
+ end
134
+ end
135
+
136
+ def pairs
137
+ hash = {}
138
+
139
+ outputs.each do |output|
140
+ hash[output["custom_id"]] = [nil ,output["content"]]
141
+ end
142
+
143
+ inputs.each do |input|
144
+ next if hash[input["custom_id"]].nil?
145
+ hash[input["custom_id"]][0] = input["content"]
146
+ end
147
+
148
+ hash.values
149
+ end
150
+
151
+ def cancel
152
+ Batch.cancel(id)
153
+ end
154
+
155
+ def enqueue
156
+ return false if GptFunction::Storage.batch_storage.nil?
157
+
158
+ GptFunction::Storage.batch_storage.enqueue(self.to_hash)
159
+ end
160
+
161
+ # validating the input file is being validated before the batch can begin
162
+ # failed the input file has failed the validation process
163
+ # in_progress the input file was successfully validated and the batch is currently being run
164
+ # finalizing the batch has completed and the results are being prepared
165
+ # completed the batch has been completed and the results are ready
166
+ # expired the batch was not able to be completed within the 24-hour time window
167
+ # cancelling the batch is being cancelled (may take up to 10 minutes)
168
+ # cancelled the batch was cancelled
169
+ def is_processed
170
+ ["failed", "completed", "expired", "cancelled"].include? status
171
+ end
172
+
173
+ class << self
174
+ def list(limit: 20, after: nil)
175
+ # 創建批次請求
176
+ uri = URI('https://api.openai.com/v1/batches')
177
+ request = Net::HTTP::Get.new(uri, 'Content-Type' => 'application/json')
178
+ request['Authorization'] = "Bearer #{GptFunction.api_key}"
179
+
180
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
181
+ http.request(request)
182
+ end
183
+
184
+ raise "Batch creation failed: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
185
+
186
+ body_hash = JSON.parse(response.body)
187
+ body_hash.dig("data").map do |hash|
188
+ Batch.new(hash)
189
+ end
190
+ end
191
+
192
+ def create(requests)
193
+ requests = requests.each_with_index.map do |request, index|
194
+ {
195
+ custom_id: "request-#{index + 1}",
196
+ method: "POST",
197
+ url: "/v1/chat/completions",
198
+ body: request,
199
+ }
200
+ end
201
+
202
+ # 上傳資料
203
+ file = File.create(requests)
204
+
205
+ # 創建批次請求
206
+ uri = URI('https://api.openai.com/v1/batches')
207
+ request = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
208
+ request['Authorization'] = "Bearer #{GptFunction.api_key}"
209
+ request.body = {
210
+ input_file_id: file.id,
211
+ endpoint: '/v1/chat/completions',
212
+ completion_window: '24h'
213
+ }.to_json
214
+
215
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
216
+ http.request(request)
217
+ end
218
+
219
+ raise "Batch creation failed: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
220
+
221
+ hash = JSON.parse(response.body)
222
+ batch = Batch.new(hash)
223
+ batch.enqueue
224
+ batch
225
+ rescue => e
226
+ file&.delete
227
+ raise e
228
+ end
229
+
230
+ def from_id(batch_id)
231
+ # 檢查批次狀態
232
+ uri = URI("https://api.openai.com/v1/batches/#{batch_id}")
233
+ request = Net::HTTP::Get.new(uri)
234
+ request['Authorization'] = "Bearer #{GptFunction.api_key}"
235
+ request['Content-Type'] = 'application/json'
236
+
237
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
238
+ http.request(request)
239
+ end
240
+
241
+ raise "Batch status check failed: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
242
+
243
+ hash = JSON.parse(response.body)
244
+ Batch.new(hash)
245
+ end
246
+
247
+ def cancel(batch_id)
248
+ uri = URI("https://api.openai.com/v1/batches/#{batch_id}/cancel")
249
+ request = Net::HTTP::Post.new(uri)
250
+ request['Authorization'] = "Bearer #{GptFunction.api_key}"
251
+ request['Content-Type'] = 'application/json'
252
+
253
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
254
+ http.request(request)
255
+ end
256
+
257
+ # {
258
+ # "error": {
259
+ # "message": "Cannot cancel a batch with status 'completed'.",
260
+ # "type": "invalid_request_error",
261
+ # "param": null,
262
+ # "code": null
263
+ # }
264
+ # }
265
+ raise "Batch cancel failed: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
266
+
267
+ response.body
268
+ end
269
+
270
+ def dequeue
271
+ hash = GptFunction::Storage.batch_storage&.dequeue
272
+ id = hash&.dig("id") || hash&.dig(:id)
273
+ from_id(id) if id
274
+ end
275
+
276
+ # 進行批次請求處理
277
+ # count: 處理批次請求的數量
278
+ # block: 處理批次請求的 block
279
+ # 返回值: 是否還有批次請求需要處理
280
+ def process(count: 1, &block)
281
+ # 從 Storage 取出 count 個批次請求
282
+ count.times do
283
+ batch = dequeue
284
+
285
+ # 如果沒有批次請求,則跳出迴圈
286
+ return false if batch.nil?
287
+
288
+ yield batch
289
+
290
+ # 如果 batch 還未處理完成,將批次請求重新加入 Storage
291
+ batch.enqueue unless batch.is_processed
292
+ end
293
+
294
+ true
295
+ end
296
+ end
297
+ end
298
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+
6
+ class GptFunction
7
+ class File
8
+ attr_reader :object
9
+ attr_reader :id
10
+ attr_reader :purpose
11
+ attr_reader :filename
12
+ attr_reader :bytes
13
+ attr_reader :created_at
14
+ attr_reader :status
15
+ attr_reader :status_details
16
+
17
+ def initialize(hash)
18
+ @object = hash["object"]
19
+ @id = hash["id"]
20
+ @purpose = hash["purpose"]
21
+ @filename = hash["filename"]
22
+ @bytes = hash["bytes"]
23
+ @created_at = hash["created_at"]
24
+ @status = hash["status"]
25
+ @status_details = hash["status_details"]
26
+ end
27
+
28
+ def to_hash
29
+ {
30
+ object: object,
31
+ id: id,
32
+ purpose: purpose,
33
+ filename: filename,
34
+ bytes: bytes,
35
+ created_at: created_at,
36
+ status: status,
37
+ status_details: status_details,
38
+ }
39
+ end
40
+
41
+ def content
42
+ File.content(id)
43
+ end
44
+
45
+ def jsonl
46
+ File.jsonl(id)
47
+ end
48
+
49
+ def delete
50
+ File.delete(id)
51
+ end
52
+
53
+ def to_s
54
+ to_hash.to_json
55
+ end
56
+
57
+ def inspect
58
+ to_hash.to_json
59
+ end
60
+
61
+ class << self
62
+ def list
63
+ uri = URI("https://api.openai.com/v1/files")
64
+ request = Net::HTTP::Get.new(uri)
65
+ request['Authorization'] = "Bearer #{GptFunction.api_key}"
66
+
67
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
68
+ http.request(request)
69
+ end
70
+
71
+ raise "File retrieval failed: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
72
+
73
+ # example response body
74
+ # {
75
+ # "object": "list",
76
+ # "data": [
77
+ # {
78
+ # "object": "file",
79
+ # "id": "file-uYu4HIFAoq0OeZDGBD5Ci8wL",
80
+ # "purpose": "batch_output",
81
+ # "filename": "batch_YMZbhJWcBYETMTfOfEf041qF_output.jsonl",
82
+ # "bytes": 1934,
83
+ # "created_at": 1722327874,
84
+ # "status": "processed",
85
+ # "status_details": null
86
+ # },
87
+ # {
88
+ # "object": "file",
89
+ # "id": "file-5AW0tCvRFKomu5s5G90yfWhs",
90
+ # "purpose": "batch",
91
+ # "filename": "batchinput.jsonl",
92
+ # "bytes": 728,
93
+ # "created_at": 1722327858,
94
+ # "status": "processed",
95
+ # "status_details": null
96
+ # },
97
+ # ]
98
+ # }
99
+ body_hash = JSON.parse(response.body)
100
+ body_hash.dig("data").map do |hash|
101
+ File.new(hash)
102
+ end
103
+ end
104
+
105
+ def create(hash_array)
106
+ # 將請求資料轉換為 JSONL 格式的字串
107
+ jsonl = hash_array.map(&:to_json).join("\n")
108
+
109
+ # 上傳資料
110
+ uri = URI('https://api.openai.com/v1/files')
111
+ request = Net::HTTP::Post.new(uri)
112
+ request['Authorization'] = "Bearer #{GptFunction.api_key}"
113
+ request['Content-Type'] = 'multipart/form-data'
114
+
115
+ # 創建 multipart form data
116
+ boundary = "CustomBoundary"
117
+ post_body = []
118
+ post_body << "--#{boundary}\r\n"
119
+ post_body << "Content-Disposition: form-data; name=\"purpose\"\r\n\r\n"
120
+ post_body << "batch\r\n"
121
+ post_body << "--#{boundary}\r\n"
122
+ post_body << "Content-Disposition: form-data; name=\"file\"; filename=\"batchinput.jsonl\"\r\n"
123
+ post_body << "Content-Type: application/json\r\n\r\n"
124
+ post_body << jsonl
125
+ post_body << "\r\n--#{boundary}--\r\n"
126
+
127
+ request.body = post_body.join
128
+ request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
129
+
130
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
131
+ http.request(request)
132
+ end
133
+
134
+ raise "File upload failed: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
135
+
136
+ hash = JSON.parse(response.body)
137
+ File.new(hash)
138
+ end
139
+
140
+ def from_id(file_id)
141
+ uri = URI("https://api.openai.com/v1/files/#{file_id}")
142
+ request = Net::HTTP::Get.new(uri)
143
+ request['Authorization'] = "Bearer #{GptFunction.api_key}"
144
+
145
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
146
+ http.request(request)
147
+ end
148
+
149
+ raise "File retrieval failed: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
150
+
151
+ hash = JSON.parse(response.body)
152
+ File.new(hash)
153
+ end
154
+
155
+ def content(file_id)
156
+ uri = URI("https://api.openai.com/v1/files/#{file_id}/content")
157
+ request = Net::HTTP::Get.new(uri)
158
+ request['Authorization'] = "Bearer #{GptFunction.api_key}"
159
+
160
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
161
+ http.request(request)
162
+ end
163
+
164
+ # {
165
+ # "error": {
166
+ # "message": "No such File object: #{file_id}",
167
+ # "type": "invalid_request_error",
168
+ # "param": "id",
169
+ # "code": null
170
+ # }
171
+ # }
172
+ raise "File retrieval failed: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
173
+ response.body
174
+ end
175
+
176
+ def jsonl(file_id)
177
+ content(file_id).split("\n").map { |line| JSON.parse(line) }
178
+ end
179
+
180
+ def delete(file_id)
181
+ uri = URI("https://api.openai.com/v1/files/#{file_id}")
182
+ request = Net::HTTP::Delete.new(uri)
183
+ request['Authorization'] = "Bearer #{GptFunction.api_key}"
184
+
185
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
186
+ http.request(request)
187
+ end
188
+
189
+ # {
190
+ # "error": {
191
+ # "message": "No such File object: file-5m1Cn4M36GOfd7bEVAoTCmcC",
192
+ # "type": "invalid_request_error",
193
+ # "param": "id",
194
+ # "code": null
195
+ # }
196
+ # }
197
+ raise "File deletion failed: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
198
+
199
+ # {"object"=>"file", "deleted"=>true, "id"=>"file-vsCH6lJkiFzi6gF9B8un3ZLT"}
200
+ JSON.parse(response.body)
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GptFunction
4
+ class SimpleQueue
5
+ def initialize
6
+ @queue = []
7
+ end
8
+
9
+ def enqueue(value)
10
+ @queue << value
11
+ true
12
+ end
13
+
14
+ def dequeue
15
+ @queue.shift
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GptFunction
4
+ module Storage
5
+ class << self
6
+ def batch_storage=(value)
7
+ # 檢查 value 有實作 enqueue 方法
8
+ raise "Invalid batch storage: should respond to #enqueue" unless value.respond_to?(:enqueue)
9
+
10
+ # 檢查 value 有實作 dequeue 方法
11
+ raise "Invalid batch storage: should respond to #dequeue" unless value.respond_to?(:dequeue)
12
+ @batch_storage = value
13
+ end
14
+
15
+ def batch_storage
16
+ @batch_storage
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GptFunction
4
+ VERSION = "0.4.0"
5
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # 這是一個簡單的 GPT 函數類別
4
+ module GptFunctions
5
+ class << self
6
+ def 翻譯成中文
7
+ GptFunction.new("Translate into Taiwanese traditional Chinese", [%w[apple 蘋果]])
8
+ end
9
+
10
+ def 擷取關鍵字
11
+ GptFunction.new("Extract all keywords",
12
+ [
13
+ [
14
+ "臺灣最新5G網路覆蓋率達95%,推動智慧城市發展,領先亞洲多國",
15
+ ["臺灣", "5G網路", "覆蓋率", "95%", "智慧城市", "發展", "領先", "亞洲", "多國"]
16
+ ]
17
+ ]
18
+ )
19
+ end
20
+
21
+ def 擷取文章標題
22
+ document = <<~DOCUMENT
23
+ 今日頭條
24
+ 科技日報|臺灣科技業最新突破,AI技術大躍進
25
+ 科技日報
26
+ 科技日報
27
+ 2023-11-17
28
+ 102
29
+ 生活新聞|臺北市最新公共交通計畫公開
30
+ 生活日報
31
+ 生活日報
32
+ 2023-11-16
33
+ 89
34
+ 健康專欄|最新研究:日常運動對心臟健康的重要性
35
+ 健康雜誌
36
+ 健康雜誌
37
+ 2023-11-15
38
+ 76
39
+ 旅遊特輯|探索臺灣東部的隱藏美食與景點
40
+ 旅遊週刊
41
+ 旅遊週刊
42
+ 2023-11-14
43
+ 65
44
+ DOCUMENT
45
+
46
+ keywords = [
47
+ "科技日報|臺灣科技業最新突破,AI技術大躍進",
48
+ "生活新聞|臺北市最新公共交通計畫公開",
49
+ "健康專欄|最新研究:日常運動對心臟健康的重要性",
50
+ "旅遊特輯|探索臺灣東部的隱藏美食與景點"
51
+ ]
52
+
53
+ GptFunction.new("Extract all titles", [[document, keywords]])
54
+ end
55
+ end
56
+ end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gpt-function
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - etrex kuo
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-19 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2024-08-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dotenv
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  description: This gem allows users to create simple and complex GPT functions for
14
28
  various applications such as translation and keyword extraction.
15
29
  email:
@@ -27,9 +41,12 @@ files:
27
41
  - Rakefile
28
42
  - gpt-function.gemspec
29
43
  - lib/gpt-function.rb
30
- - lib/gpt/function.rb
31
- - lib/gpt/function/version.rb
32
- - lib/gpt/functions.rb
44
+ - lib/gpt_function/batch.rb
45
+ - lib/gpt_function/file.rb
46
+ - lib/gpt_function/simple_queue.rb
47
+ - lib/gpt_function/storage.rb
48
+ - lib/gpt_function/version.rb
49
+ - lib/gpt_functions.rb
33
50
  - workflows/main.yml
34
51
  homepage: https://github.com/etrex/gpt-function
35
52
  licenses:
@@ -52,7 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
52
69
  - !ruby/object:Gem::Version
53
70
  version: '0'
54
71
  requirements: []
55
- rubygems_version: 3.4.10
72
+ rubygems_version: 3.5.6
56
73
  signing_key:
57
74
  specification_version: 4
58
75
  summary: A Ruby gem for creating simple GPT-based functions.
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Gpt
4
- class Function
5
- VERSION = "0.2.0"
6
- end
7
- end
data/lib/gpt/function.rb DELETED
@@ -1,90 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "net/http"
4
- require "json"
5
- require_relative "function/version"
6
-
7
- module Gpt
8
- # 這是一個簡單的 GPT 函數類別
9
- class Function
10
- @api_key = nil
11
- @model = nil
12
-
13
- class << self
14
- attr_accessor :api_key, :model
15
-
16
- def configure(api_key:, model:)
17
- @api_key = api_key
18
- @model = model
19
- end
20
- end
21
-
22
- def initialize(prompt, examples = [], temperature = 0)
23
- @temperature = temperature
24
- @messages = [
25
- {
26
- role: "system",
27
- content: "#{prompt}\n Note: The response format is always a JSON with the key output like this:{output: ...}"
28
- },
29
- *examples.flat_map do |example|
30
- [
31
- {
32
- role: "user",
33
- content: example[0]
34
- },
35
- {
36
- role: "assistant",
37
- content: { output: example[1] }.to_json
38
- }
39
- ]
40
- end
41
- ]
42
- end
43
-
44
- def call(input)
45
- # 使用類別級別的變量來發送請求
46
- response = send_request(input)
47
- body = response.body.force_encoding("UTF-8")
48
- json = JSON.parse(body)
49
- # 處理可能的錯誤回應
50
- raise StandardError, json.dig("error", "message") if json.dig("error", "code")
51
-
52
- # 處理正常的回應
53
- JSON.parse(json.dig("choices", 0, "message", "content"))["output"]
54
- end
55
-
56
- private
57
-
58
- def send_request(input)
59
- uri = URI.parse("https://api.openai.com/v1/chat/completions")
60
- request = Net::HTTP::Post.new(uri)
61
- request.content_type = "application/json"
62
- request["Authorization"] = "Bearer #{Function.api_key}"
63
- request.body = {
64
- model: Function.model,
65
- response_format: {
66
- type: "json_object"
67
- },
68
- seed: 0,
69
- messages: [
70
- *@messages,
71
- {
72
- "role": "user",
73
- "content": input
74
- }
75
- ],
76
- temperature: @temperature
77
- }.to_json
78
-
79
- req_options = {
80
- use_ssl: uri.scheme == "https",
81
- open_timeout: 60, # opening a connection timeout
82
- read_timeout: 300 # reading one block of response timeout
83
- }
84
-
85
- Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
86
- http.request(request)
87
- end
88
- end
89
- end
90
- end
data/lib/gpt/functions.rb DELETED
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Gpt
4
- # 這是一個簡單的 GPT 函數類別
5
- module Functions
6
- class << self
7
- def 翻譯成中文(input)
8
- Gpt::Function.new("Translate into Taiwanese traditional Chinese", [%w[apple 蘋果]]).call(input)
9
- end
10
-
11
- def 擷取關鍵字(input)
12
- Gpt::Function.new("Extract all keywords",
13
- [
14
- [
15
- "臺灣最新5G網路覆蓋率達95%,推動智慧城市發展,領先亞洲多國",
16
- %w[臺灣 5G網路 覆蓋率 智慧城市 亞洲]
17
- ]
18
- ]).call(input)
19
- end
20
-
21
- def 擷取文章標題(input)
22
- document = <<~DOCUMENT
23
- 今日頭條
24
- 科技日報|臺灣科技業最新突破,AI技術大躍進
25
- 科技日報
26
- 科技日報
27
- 2023-11-17
28
- 102
29
- 生活新聞|臺北市最新公共交通計畫公開
30
- 生活日報
31
- 生活日報
32
- 2023-11-16
33
- 89
34
- 健康專欄|最新研究:日常運動對心臟健康的重要性
35
- 健康雜誌
36
- 健康雜誌
37
- 2023-11-15
38
- 76
39
- 旅遊特輯|探索臺灣東部的隱藏美食與景點
40
- 旅遊週刊
41
- 旅遊週刊
42
- 2023-11-14
43
- 65
44
- DOCUMENT
45
-
46
- keywords = [
47
- "科技日報|臺灣科技業最新突破,AI技術大躍進",
48
- "生活新聞|臺北市最新公共交通計畫公開",
49
- "健康專欄|最新研究:日常運動對心臟健康的重要性",
50
- "旅遊特輯|探索臺灣東部的隱藏美食與景點"
51
- ]
52
-
53
- Gpt::Function.new("Extract all titles", [[document, keywords]]).call(input)
54
- end
55
- end
56
- end
57
- end