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 +4 -4
- data/Gemfile.lock +3 -1
- data/README.md +88 -19
- data/gpt-function.gemspec +4 -2
- data/lib/gpt-function.rb +123 -2
- data/lib/gpt_function/batch.rb +298 -0
- data/lib/gpt_function/file.rb +204 -0
- data/lib/gpt_function/simple_queue.rb +18 -0
- data/lib/gpt_function/storage.rb +20 -0
- data/lib/gpt_function/version.rb +5 -0
- data/lib/gpt_functions.rb +56 -0
- metadata +24 -7
- data/lib/gpt/function/version.rb +0 -7
- data/lib/gpt/function.rb +0 -90
- data/lib/gpt/functions.rb +0 -57
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cdeb301d12f6247b694accec4654d4d65bab0ca071cb4bf7ae73631bf540bc56
|
4
|
+
data.tar.gz: dd5cb067931ac6411a545641b96f40ef81d929bd1d388cfe93808d5c76b6e2ec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
-
|
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
|
-
|
32
|
+
# 使用內建的翻譯方法
|
33
|
+
p GptFunctions.翻譯成中文.call("banana") # "香蕉"
|
31
34
|
|
32
|
-
#
|
33
|
-
|
35
|
+
# 使用內建的擷取關鍵字方法
|
36
|
+
p GptFunctions.擷取關鍵字.call("臺北市政府推動綠色交通計劃,鼓勵民眾使用公共運輸和自行車") # ["臺北市政府", "綠色交通計劃", "民眾", "公共運輸", "自行車"]
|
34
37
|
|
35
|
-
#
|
36
|
-
|
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
|
-
|
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
|
-
|
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/
|
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 =
|
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
|
-
|
4
|
-
|
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,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,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.
|
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:
|
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/
|
31
|
-
- lib/
|
32
|
-
- lib/
|
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.
|
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.
|
data/lib/gpt/function/version.rb
DELETED
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
|