gpt-function 0.2.0 → 0.4.0

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