gpt 0.0.1 → 0.1.1
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/CHANGELOG.md +10 -0
- data/README.md +80 -17
- data/lib/gpt/client.rb +6 -3
- data/lib/gpt/response_extender.rb +74 -0
- data/lib/gpt/responses.rb +49 -2
- data/lib/gpt/version.rb +5 -0
- data/lib/gpt.rb +22 -0
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 840186b5978b9ff54b0aa11256b238bfe03dba6d50020e70ace54dc9ad69e95c
|
4
|
+
data.tar.gz: 63ea299d5a2d5f0c119e99605cde359294bc60de72fe84eadfd704288c03eb06
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7aa46b3f49131432ee7e33303d9581a391d7eea15e75b9ff7e5e1ef78ed33a5c720f337cdafb9641d62a8a11bac427b222fcaa8e9d473f4040559cd7c6e8769c
|
7
|
+
data.tar.gz: f8a4865378bd4981362d751e9c4140618bb2451b90c408a92a3ba95ee391138168c3fe26dcbe73211b867d7547b3ac6803f57b5a989d19e4021488e4c5365e74
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 0.1.0
|
4
|
+
- Suporte e exemplos para GPT-5 (reasoning minimal, text verbosity, custom tools e allowed_tools)
|
5
|
+
- Validação de `OPENAI_API_KEY` e ajuste de User-Agent
|
6
|
+
- README atualizado para GPT-5
|
7
|
+
|
8
|
+
## 0.1.1
|
9
|
+
- Adiciona `GPT.ask` para uso simplificado, com suporte a streaming
|
10
|
+
- Adiciona `GPT::ResponseExtender` com helpers (`content`, `usage`, `total_tokens`, `to_h`)
|
11
|
+
- `Responses#create/get` passam a estender a resposta com os helpers
|
12
|
+
|
3
13
|
## 0.0.1
|
4
14
|
- Primeira versão com cliente para API Responses (create/get/delete/cancel/input_items) e streaming SSE.
|
5
15
|
|
data/README.md
CHANGED
@@ -1,39 +1,93 @@
|
|
1
1
|
# gpt
|
2
2
|
|
3
|
-
Cliente Ruby simples para a API
|
3
|
+
Cliente Ruby simples para a Responses API, com foco no GPT-5, com uma API de alto nível inspirada no OpenAIExt.
|
4
4
|
|
5
|
-
Instalação
|
5
|
+
## Instalação
|
6
6
|
```
|
7
|
-
|
7
|
+
bash build_and_install.sh
|
8
8
|
```
|
9
9
|
|
10
|
-
|
10
|
+
## Configuração
|
11
|
+
- Defina `OPENAI_API_KEY` ou `OPENAI_ACCESS_TOKEN` no ambiente.
|
12
|
+
- Opcional: `OPENAI_ORG_ID` ou `OPENAI_ORGANIZATION_ID`, `OPENAI_PROJECT_ID`.
|
13
|
+
- Opcional: `OPENAI_REQUEST_TIMEOUT` (segundos, padrão 120).
|
14
|
+
|
15
|
+
## Uso básico (GPT-5)
|
11
16
|
```ruby
|
12
17
|
require 'gpt'
|
13
18
|
|
14
|
-
|
19
|
+
res = GPT.ask('Diga olá em uma frase.', model: 'gpt-5')
|
20
|
+
puts res.content
|
21
|
+
```
|
22
|
+
|
23
|
+
## Reasoning mínimo (minimal)
|
24
|
+
```ruby
|
25
|
+
res = GPT.responses.create({
|
26
|
+
'model' => 'gpt-5',
|
27
|
+
'input' => 'Quanto ouro seria necessário para cobrir a Estátua da Liberdade com 1mm?',
|
28
|
+
'reasoning' => { 'effort' => 'minimal' }
|
29
|
+
})
|
30
|
+
```
|
31
|
+
|
32
|
+
## Verbosidade baixa
|
33
|
+
```ruby
|
34
|
+
res = GPT.responses.create({
|
35
|
+
'model' => 'gpt-5',
|
36
|
+
'input' => 'Qual é a resposta para a vida, o universo e tudo mais?',
|
37
|
+
'text' => { 'verbosity' => 'low' }
|
38
|
+
})
|
39
|
+
```
|
40
|
+
|
41
|
+
## Ferramentas personalizadas (custom tools)
|
42
|
+
```ruby
|
43
|
+
res = GPT.responses.create({
|
44
|
+
'model' => 'gpt-5',
|
45
|
+
'input' => 'Use a ferramenta code_exec para calcular a área de um círculo com raio igual ao número de letras r em blueberry',
|
46
|
+
'tools' => [
|
47
|
+
{ 'type' => 'custom', 'name' => 'code_exec', 'description' => 'Executa código Python arbitrário' }
|
48
|
+
]
|
49
|
+
})
|
50
|
+
```
|
51
|
+
|
52
|
+
## Restringindo ferramentas (allowed_tools)
|
53
|
+
```ruby
|
54
|
+
res = GPT.responses.create({
|
55
|
+
'model' => 'gpt-5',
|
56
|
+
'input' => 'Como está o tempo em São Paulo?',
|
57
|
+
'tools' => [ { 'type' => 'function', 'name' => 'get_weather' } ],
|
58
|
+
'tool_choice' => {
|
59
|
+
'type' => 'allowed_tools',
|
60
|
+
'mode' => 'auto',
|
61
|
+
'tools' => [ { 'type' => 'function', 'name' => 'get_weather' } ]
|
62
|
+
}
|
63
|
+
})
|
64
|
+
```
|
15
65
|
|
16
|
-
|
17
|
-
|
18
|
-
|
66
|
+
## Passando raciocínio prévio (previous_response_id)
|
67
|
+
```ruby
|
68
|
+
first = GPT.responses.create({
|
69
|
+
'model' => 'gpt-5',
|
70
|
+
'input' => 'Planeje passos para resolver X.'
|
19
71
|
})
|
20
72
|
|
21
|
-
|
73
|
+
followup = GPT.responses.create({
|
74
|
+
'model' => 'gpt-5',
|
75
|
+
'input' => 'Agora execute o primeiro passo.',
|
76
|
+
'previous_response_id' => first['id']
|
77
|
+
})
|
22
78
|
```
|
23
79
|
|
24
|
-
Streaming SSE
|
80
|
+
## Streaming SSE
|
25
81
|
```ruby
|
26
82
|
require 'gpt'
|
27
83
|
|
28
|
-
GPT.
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
print chunk
|
33
|
-
end
|
84
|
+
GPT.ask('Conte uma história curta.', model: 'gpt-5', stream: true) { |chunk| print chunk }
|
85
|
+
|
86
|
+
# Streaming de texto direto
|
87
|
+
GPT.ask('Conte uma história curta.', model: 'gpt-5', text_stream: true) { |text| print text }
|
34
88
|
```
|
35
89
|
|
36
|
-
Outras operações
|
90
|
+
## Outras operações
|
37
91
|
```ruby
|
38
92
|
id = res['id']
|
39
93
|
GPT.responses.get(id)
|
@@ -41,3 +95,12 @@ GPT.responses.input_items(id)
|
|
41
95
|
GPT.responses.cancel(id)
|
42
96
|
GPT.responses.delete(id)
|
43
97
|
```
|
98
|
+
|
99
|
+
## Helpers de resposta
|
100
|
+
```ruby
|
101
|
+
res = GPT.ask('Qual a capital da França?', model: 'gpt-5')
|
102
|
+
res.content
|
103
|
+
res.model
|
104
|
+
res.total_tokens
|
105
|
+
res.to_h
|
106
|
+
```
|
data/lib/gpt/client.rb
CHANGED
@@ -5,10 +5,10 @@ module GPT
|
|
5
5
|
|
6
6
|
attr_reader :api_key, :base_url, :timeout, :organization, :project
|
7
7
|
|
8
|
-
def initialize(api_key: ENV['OPENAI_API_KEY'], base_url: nil, timeout: nil, organization: ENV['OPENAI_ORG_ID'], project: ENV['OPENAI_PROJECT_ID'])
|
8
|
+
def initialize(api_key: ENV['OPENAI_API_KEY'] || ENV['OPENAI_ACCESS_TOKEN'], base_url: nil, timeout: nil, organization: ENV['OPENAI_ORG_ID'] || ENV['OPENAI_ORGANIZATION_ID'], project: ENV['OPENAI_PROJECT_ID'])
|
9
9
|
@api_key = api_key
|
10
10
|
@base_url = base_url || DEFAULT_BASE_URL
|
11
|
-
@timeout = (timeout || DEFAULT_TIMEOUT).to_i
|
11
|
+
@timeout = (timeout || ENV['OPENAI_REQUEST_TIMEOUT'] || DEFAULT_TIMEOUT).to_i
|
12
12
|
@organization = organization
|
13
13
|
@project = project
|
14
14
|
end
|
@@ -86,10 +86,13 @@ module GPT
|
|
86
86
|
end
|
87
87
|
|
88
88
|
def apply_headers(req)
|
89
|
+
if !api_key || api_key.empty?
|
90
|
+
raise GPT::Error.new('Defina OPENAI_API_KEY ou OPENAI_ACCESS_TOKEN')
|
91
|
+
end
|
89
92
|
req['Authorization'] = "Bearer #{api_key}"
|
90
93
|
req['OpenAI-Organization'] = organization if organization && !organization.empty?
|
91
94
|
req['OpenAI-Project'] = project if project && !project.empty?
|
92
|
-
req['User-Agent'] =
|
95
|
+
req['User-Agent'] = "gpt-ruby/#{GPT::VERSION}"
|
93
96
|
end
|
94
97
|
|
95
98
|
def parse_response(res)
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module GPT
|
2
|
+
module ResponseExtender
|
3
|
+
def message
|
4
|
+
if self['choices']
|
5
|
+
dig('choices', 0, 'message') || {}
|
6
|
+
else
|
7
|
+
output_message = if self['output'].is_a?(Array)
|
8
|
+
self['output'].find { |i| i['type'] == 'message' } || self['output'].first
|
9
|
+
end
|
10
|
+
output_message || {}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def content
|
15
|
+
if self['choices']
|
16
|
+
dig('choices', 0, 'message', 'content')
|
17
|
+
else
|
18
|
+
msg = message
|
19
|
+
contents = msg && msg['content']
|
20
|
+
return nil unless contents.is_a?(Array)
|
21
|
+
text_item = contents.find { |c| c['type'] == 'output_text' || c['type'] == 'text' }
|
22
|
+
text_item && text_item['text']
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def content?
|
27
|
+
!content.nil? && !content.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def usage
|
31
|
+
self['usage'] || {}
|
32
|
+
end
|
33
|
+
|
34
|
+
def prompt_tokens
|
35
|
+
usage['prompt_tokens'] || 0
|
36
|
+
end
|
37
|
+
|
38
|
+
def completion_tokens
|
39
|
+
usage['completion_tokens'] || 0
|
40
|
+
end
|
41
|
+
|
42
|
+
def total_tokens
|
43
|
+
usage['total_tokens'] || 0
|
44
|
+
end
|
45
|
+
|
46
|
+
def model
|
47
|
+
self['model']
|
48
|
+
end
|
49
|
+
|
50
|
+
def created_at
|
51
|
+
if self['created']
|
52
|
+
Time.at(self['created'])
|
53
|
+
elsif self['created_at']
|
54
|
+
Time.at(self['created_at'])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_h
|
59
|
+
{
|
60
|
+
content: content,
|
61
|
+
role: message['role'],
|
62
|
+
model: model,
|
63
|
+
usage: usage,
|
64
|
+
created_at: created_at
|
65
|
+
}.compact
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_s
|
69
|
+
content || '[No content]'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
|
data/lib/gpt/responses.rb
CHANGED
@@ -5,7 +5,9 @@ module GPT
|
|
5
5
|
end
|
6
6
|
|
7
7
|
def create(payload)
|
8
|
-
@client.json_post('/v1/responses', body: payload)
|
8
|
+
res = @client.json_post('/v1/responses', body: payload)
|
9
|
+
res.extend(GPT::ResponseExtender) if res.is_a?(Hash)
|
10
|
+
res
|
9
11
|
end
|
10
12
|
|
11
13
|
def get(response_id, include: nil, include_obfuscation: nil, starting_after: nil, stream: nil)
|
@@ -14,7 +16,9 @@ module GPT
|
|
14
16
|
query['include_obfuscation'] = include_obfuscation unless include_obfuscation.nil?
|
15
17
|
query['starting_after'] = starting_after if starting_after
|
16
18
|
query['stream'] = stream unless stream.nil?
|
17
|
-
@client.json_get("/v1/responses/#{response_id}", query: query)
|
19
|
+
res = @client.json_get("/v1/responses/#{response_id}", query: query)
|
20
|
+
res.extend(GPT::ResponseExtender) if res.is_a?(Hash)
|
21
|
+
res
|
18
22
|
end
|
19
23
|
|
20
24
|
def delete(response_id)
|
@@ -42,6 +46,49 @@ module GPT
|
|
42
46
|
yield chunk if block_given?
|
43
47
|
end
|
44
48
|
end
|
49
|
+
|
50
|
+
def stream_text(payload)
|
51
|
+
buffer = ''.dup
|
52
|
+
stream(payload) do |chunk|
|
53
|
+
buffer << chunk
|
54
|
+
parts = buffer.split("\n\n", -1)
|
55
|
+
buffer = parts.pop || ''.dup
|
56
|
+
parts.each do |raw_event|
|
57
|
+
lines = raw_event.split("\n")
|
58
|
+
event_name = nil
|
59
|
+
data_lines = []
|
60
|
+
lines.each do |line|
|
61
|
+
if line.start_with?('event:')
|
62
|
+
event_name = line.sub('event:', '').strip
|
63
|
+
elsif line.start_with?('data:')
|
64
|
+
data_lines << line.sub('data:', '').strip
|
65
|
+
end
|
66
|
+
end
|
67
|
+
next if data_lines.empty?
|
68
|
+
data = data_lines.join("\n")
|
69
|
+
next if data == '[DONE]'
|
70
|
+
begin
|
71
|
+
json = Oj.load(data)
|
72
|
+
rescue Oj::ParseError
|
73
|
+
next
|
74
|
+
end
|
75
|
+
case event_name
|
76
|
+
when 'response.output_text.delta'
|
77
|
+
delta = json['delta']
|
78
|
+
yield delta if delta && !delta.empty?
|
79
|
+
when 'response.delta'
|
80
|
+
delta = json.dig('delta', 'content')
|
81
|
+
if delta.is_a?(Array)
|
82
|
+
text_piece = delta.find { |c| c['type'] == 'output_text' || c['type'] == 'text' }
|
83
|
+
yield(text_piece['text']) if text_piece && text_piece['text'] && !text_piece['text'].empty?
|
84
|
+
end
|
85
|
+
else
|
86
|
+
# ignore other events
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
true
|
91
|
+
end
|
45
92
|
end
|
46
93
|
end
|
47
94
|
|
data/lib/gpt.rb
CHANGED
@@ -1,10 +1,14 @@
|
|
1
|
+
module GPT; end
|
2
|
+
|
1
3
|
require 'oj'
|
2
4
|
require 'net/http'
|
3
5
|
require 'uri'
|
4
6
|
|
7
|
+
require_relative 'gpt/version'
|
5
8
|
require_relative 'gpt/error'
|
6
9
|
require_relative 'gpt/client'
|
7
10
|
require_relative 'gpt/responses'
|
11
|
+
require_relative 'gpt/response_extender'
|
8
12
|
|
9
13
|
module GPT
|
10
14
|
def self.client
|
@@ -14,6 +18,24 @@ module GPT
|
|
14
18
|
def self.responses
|
15
19
|
@responses ||= Responses.new(client)
|
16
20
|
end
|
21
|
+
|
22
|
+
def self.ask(prompt, model: 'gpt-5', stream: false, text_stream: false, **opts, &block)
|
23
|
+
payload = { 'model' => model, 'input' => prompt }
|
24
|
+
opts.each { |k, v| payload[k.to_s] = v }
|
25
|
+
if stream
|
26
|
+
responses.stream(payload) do |chunk|
|
27
|
+
yield chunk if block_given?
|
28
|
+
end
|
29
|
+
elsif text_stream
|
30
|
+
responses.stream_text(payload) do |text|
|
31
|
+
yield text if block_given?
|
32
|
+
end
|
33
|
+
else
|
34
|
+
res = responses.create(payload)
|
35
|
+
res.extend(ResponseExtender)
|
36
|
+
res
|
37
|
+
end
|
38
|
+
end
|
17
39
|
end
|
18
40
|
|
19
41
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gpt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gedean Dias
|
@@ -93,8 +93,8 @@ dependencies:
|
|
93
93
|
- - "~>"
|
94
94
|
- !ruby/object:Gem::Version
|
95
95
|
version: '0.9'
|
96
|
-
description:
|
97
|
-
|
96
|
+
description: Cliente Ruby simples para a Responses API com suporte aos recursos do
|
97
|
+
GPT-5 (reasoning, verbosity, tools).
|
98
98
|
email: gedean.dias@gmail.com
|
99
99
|
executables: []
|
100
100
|
extensions: []
|
@@ -106,7 +106,9 @@ files:
|
|
106
106
|
- lib/gpt.rb
|
107
107
|
- lib/gpt/client.rb
|
108
108
|
- lib/gpt/error.rb
|
109
|
+
- lib/gpt/response_extender.rb
|
109
110
|
- lib/gpt/responses.rb
|
111
|
+
- lib/gpt/version.rb
|
110
112
|
homepage: https://github.com/gedean/openaiext
|
111
113
|
licenses:
|
112
114
|
- MIT
|
@@ -127,5 +129,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
127
129
|
requirements: []
|
128
130
|
rubygems_version: 3.7.1
|
129
131
|
specification_version: 4
|
130
|
-
summary: GPT
|
132
|
+
summary: Cliente Ruby para GPT-5 (Responses API)
|
131
133
|
test_files: []
|