exa-ai-ruby 1.0.0 → 1.1.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/CHANGELOG.md +11 -0
- data/README.md +79 -16
- data/exe/exa +14 -0
- data/lib/exa/cli/account_resolver.rb +53 -0
- data/lib/exa/cli/config_store.rb +97 -0
- data/lib/exa/cli/root.rb +802 -0
- data/lib/exa/cli.rb +5 -0
- data/lib/exa/internal/transport/base_client.rb +49 -9
- data/lib/exa/internal/transport/pooled_net_requester.rb +10 -1
- data/lib/exa/resources/search.rb +21 -1
- data/lib/exa/resources/websets.rb +17 -0
- data/lib/exa/responses/answer_response.rb +77 -0
- data/lib/exa/responses/search_response.rb +15 -2
- data/lib/exa/responses.rb +1 -0
- data/lib/exa/types/answer.rb +1 -0
- data/lib/exa/version.rb +1 -1
- metadata +93 -2
data/lib/exa/cli.rb
ADDED
|
@@ -44,16 +44,19 @@ module Exa
|
|
|
44
44
|
@requester = requester
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
def request(method:, path:, query: nil, headers: nil, body: nil, unwrap: nil, stream: false, response_model: nil)
|
|
47
|
+
def request(method:, path:, query: nil, headers: nil, body: nil, unwrap: nil, stream: false, response_model: nil, request_options: nil)
|
|
48
|
+
options = normalize_request_options(request_options)
|
|
48
49
|
req = build_request(
|
|
49
50
|
method: method,
|
|
50
51
|
path: Array(path).join("/"),
|
|
51
52
|
query: query,
|
|
52
53
|
headers: headers,
|
|
53
|
-
body: body
|
|
54
|
+
body: body,
|
|
55
|
+
request_timeout: options[:timeout],
|
|
56
|
+
idempotency_key: options[:idempotency_key]
|
|
54
57
|
)
|
|
55
58
|
|
|
56
|
-
_, response, stream_enum = send_request(req)
|
|
59
|
+
_, response, stream_enum = send_request(req, max_retries: options[:max_retries] || max_retries)
|
|
57
60
|
parsed_headers = Exa::Internal::Util.normalized_headers(response.each_header.to_h)
|
|
58
61
|
|
|
59
62
|
if stream
|
|
@@ -77,13 +80,14 @@ module Exa
|
|
|
77
80
|
cleaned.join("/")
|
|
78
81
|
end
|
|
79
82
|
|
|
80
|
-
def build_request(method:, path:, query:, headers:, body:)
|
|
83
|
+
def build_request(method:, path:, query:, headers:, body:, request_timeout:, idempotency_key:)
|
|
81
84
|
normalized_path = normalize_path(path)
|
|
82
85
|
url = @base_url + normalized_path
|
|
83
86
|
url.query = Exa::Internal::Util.build_query(query)
|
|
84
87
|
|
|
85
88
|
header_overrides = headers ? headers.each_with_object({}) { |(k, v), acc| acc[k] = v unless v.nil? } : {}
|
|
86
89
|
final_headers = PLATFORM_HEADERS.merge(default_headers).merge(header_overrides)
|
|
90
|
+
final_headers["idempotency-key"] ||= idempotency_key if idempotency_key
|
|
87
91
|
|
|
88
92
|
payload = case body
|
|
89
93
|
when nil
|
|
@@ -96,12 +100,14 @@ module Exa
|
|
|
96
100
|
JSON.generate(body)
|
|
97
101
|
end
|
|
98
102
|
|
|
103
|
+
effective_timeout = request_timeout || timeout
|
|
104
|
+
|
|
99
105
|
{
|
|
100
106
|
method: method,
|
|
101
107
|
url: url,
|
|
102
108
|
headers: final_headers,
|
|
103
109
|
body: payload,
|
|
104
|
-
deadline: Process.clock_gettime(Process::CLOCK_MONOTONIC) +
|
|
110
|
+
deadline: Process.clock_gettime(Process::CLOCK_MONOTONIC) + effective_timeout,
|
|
105
111
|
max_retries: max_retries
|
|
106
112
|
}
|
|
107
113
|
end
|
|
@@ -118,11 +124,11 @@ module Exa
|
|
|
118
124
|
{}
|
|
119
125
|
end
|
|
120
126
|
|
|
121
|
-
def send_request(request, retry_count: 0)
|
|
127
|
+
def send_request(request, retry_count: 0, max_retries: @max_retries)
|
|
122
128
|
status, response, body_enum = @requester.execute(request)
|
|
123
129
|
if should_retry?(status) && retry_count < max_retries
|
|
124
|
-
sleep(retry_delay(retry_count))
|
|
125
|
-
return send_request(request, retry_count: retry_count + 1)
|
|
130
|
+
sleep(retry_delay(retry_count, response))
|
|
131
|
+
return send_request(request, retry_count: retry_count + 1, max_retries: max_retries)
|
|
126
132
|
end
|
|
127
133
|
|
|
128
134
|
if status >= 400
|
|
@@ -149,11 +155,36 @@ module Exa
|
|
|
149
155
|
[408, 409, 429].include?(status) || status >= 500
|
|
150
156
|
end
|
|
151
157
|
|
|
152
|
-
def retry_delay(retry_count)
|
|
158
|
+
def retry_delay(retry_count, response = nil)
|
|
159
|
+
header_delay = retry_after_delay(response)
|
|
160
|
+
return header_delay if header_delay
|
|
161
|
+
|
|
153
162
|
delay = initial_retry_delay * (2**retry_count)
|
|
154
163
|
[delay, max_retry_delay].min
|
|
155
164
|
end
|
|
156
165
|
|
|
166
|
+
def retry_after_delay(response)
|
|
167
|
+
return nil unless response
|
|
168
|
+
value = nil
|
|
169
|
+
if response.respond_to?(:[])
|
|
170
|
+
value = response["Retry-After"] || response["retry-after"]
|
|
171
|
+
end
|
|
172
|
+
unless value
|
|
173
|
+
if response.respond_to?(:each_header)
|
|
174
|
+
response.each_header do |k, v|
|
|
175
|
+
if k.downcase == "retry-after"
|
|
176
|
+
value = v
|
|
177
|
+
break
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
return nil unless value
|
|
183
|
+
parsed = Integer(value) rescue Float(value) rescue nil
|
|
184
|
+
return nil unless parsed
|
|
185
|
+
[parsed.to_f, max_retry_delay].min
|
|
186
|
+
end
|
|
187
|
+
|
|
157
188
|
def dig(obj, path)
|
|
158
189
|
Array(path).reduce(obj) do |memo, key|
|
|
159
190
|
memo.is_a?(Hash) ? memo[key] : nil
|
|
@@ -165,6 +196,15 @@ module Exa
|
|
|
165
196
|
return model.from_hash(data) if model.respond_to?(:from_hash)
|
|
166
197
|
model.new(data)
|
|
167
198
|
end
|
|
199
|
+
|
|
200
|
+
def normalize_request_options(options)
|
|
201
|
+
return {} if options.nil?
|
|
202
|
+
opts = {}
|
|
203
|
+
opts[:timeout] = options[:timeout] if options[:timeout]
|
|
204
|
+
opts[:max_retries] = options[:max_retries] if options[:max_retries]
|
|
205
|
+
opts[:idempotency_key] = options[:idempotency_key] if options[:idempotency_key]
|
|
206
|
+
opts
|
|
207
|
+
end
|
|
168
208
|
end
|
|
169
209
|
end
|
|
170
210
|
end
|
|
@@ -47,7 +47,16 @@ module Exa
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
_, response = enum.next
|
|
50
|
-
|
|
50
|
+
body_stream = Enumerator.new do |y|
|
|
51
|
+
loop do
|
|
52
|
+
begin
|
|
53
|
+
y << enum.next
|
|
54
|
+
rescue StopIteration
|
|
55
|
+
break
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
body = Exa::Internal::Util.fused_enum(body_stream)
|
|
51
60
|
[Integer(response.code), response, body]
|
|
52
61
|
end
|
|
53
62
|
end
|
data/lib/exa/resources/search.rb
CHANGED
|
@@ -21,9 +21,16 @@ module Exa
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def answer(params)
|
|
24
|
+
stream = stream_requested?(params)
|
|
24
25
|
normalized = normalize_nested_struct(params, :search_options, Exa::Types::AnswerSearchOptions)
|
|
25
26
|
payload = serialize(Exa::Types::AnswerRequest, normalized)
|
|
26
|
-
client.request(
|
|
27
|
+
client.request(
|
|
28
|
+
method: :post,
|
|
29
|
+
path: "answer",
|
|
30
|
+
body: payload,
|
|
31
|
+
stream: stream,
|
|
32
|
+
response_model: stream ? nil : Exa::Responses::AnswerResponse
|
|
33
|
+
)
|
|
27
34
|
end
|
|
28
35
|
|
|
29
36
|
private
|
|
@@ -39,6 +46,19 @@ module Exa
|
|
|
39
46
|
merged.delete(key.to_s)
|
|
40
47
|
merged
|
|
41
48
|
end
|
|
49
|
+
|
|
50
|
+
def stream_requested?(params)
|
|
51
|
+
case params
|
|
52
|
+
when Hash
|
|
53
|
+
value = params[:stream]
|
|
54
|
+
value = params["stream"] if value.nil?
|
|
55
|
+
!!value
|
|
56
|
+
when Exa::Types::AnswerRequest
|
|
57
|
+
!!params.stream
|
|
58
|
+
else
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
end
|
|
42
62
|
end
|
|
43
63
|
end
|
|
44
64
|
end
|
|
@@ -61,6 +61,23 @@ module Exa
|
|
|
61
61
|
)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
def cancel(webset_id)
|
|
65
|
+
client.request(
|
|
66
|
+
method: :post,
|
|
67
|
+
path: websets_path(webset_id, "cancel"),
|
|
68
|
+
response_model: Exa::Responses::Webset
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def preview(params)
|
|
73
|
+
client.request(
|
|
74
|
+
method: :post,
|
|
75
|
+
path: ["v0", "websets", "preview"],
|
|
76
|
+
body: params,
|
|
77
|
+
response_model: Exa::Responses::Webset
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
64
81
|
private
|
|
65
82
|
|
|
66
83
|
def websets_path(*parts)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Exa
|
|
4
|
+
module Responses
|
|
5
|
+
class AnswerCitation < T::Struct
|
|
6
|
+
const :id, T.nilable(String)
|
|
7
|
+
const :url, T.nilable(String)
|
|
8
|
+
const :title, T.nilable(String)
|
|
9
|
+
const :author, T.nilable(String)
|
|
10
|
+
const :published_date, T.nilable(String)
|
|
11
|
+
const :text, T.nilable(String)
|
|
12
|
+
const :image, T.nilable(String)
|
|
13
|
+
const :favicon, T.nilable(String)
|
|
14
|
+
|
|
15
|
+
def self.from_hash(hash)
|
|
16
|
+
sym = Helpers.symbolize_keys(hash)
|
|
17
|
+
new(
|
|
18
|
+
id: sym[:id],
|
|
19
|
+
url: sym[:url],
|
|
20
|
+
title: sym[:title],
|
|
21
|
+
author: sym[:author],
|
|
22
|
+
published_date: sym[:publishedDate],
|
|
23
|
+
text: sym[:text],
|
|
24
|
+
image: sym[:image],
|
|
25
|
+
favicon: sym[:favicon]
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class CostBreakdown < T::Struct
|
|
31
|
+
const :search, T.nilable(Float)
|
|
32
|
+
const :contents, T.nilable(Float)
|
|
33
|
+
const :breakdown, T.nilable(T::Hash[Symbol, T.nilable(Float)])
|
|
34
|
+
|
|
35
|
+
def self.from_hash(hash)
|
|
36
|
+
return nil unless hash
|
|
37
|
+
sym = Helpers.symbolize_keys(hash)
|
|
38
|
+
new(
|
|
39
|
+
search: sym[:search]&.to_f,
|
|
40
|
+
contents: sym[:contents]&.to_f,
|
|
41
|
+
breakdown: sym[:breakdown]&.transform_keys(&:to_sym)
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class CostDollars < T::Struct
|
|
47
|
+
const :total, T.nilable(Float)
|
|
48
|
+
const :break_down, T.nilable(T::Array[CostBreakdown])
|
|
49
|
+
const :per_request_prices, T.nilable(T::Hash[Symbol, T.untyped])
|
|
50
|
+
|
|
51
|
+
def self.from_hash(hash)
|
|
52
|
+
return nil unless hash
|
|
53
|
+
sym = Helpers.symbolize_keys(hash)
|
|
54
|
+
new(
|
|
55
|
+
total: sym[:total]&.to_f,
|
|
56
|
+
break_down: sym[:breakDown]&.map { CostBreakdown.from_hash(_1) },
|
|
57
|
+
per_request_prices: sym[:perRequestPrices]&.transform_keys(&:to_sym)
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class AnswerResponse < T::Struct
|
|
63
|
+
const :answer, T.nilable(String)
|
|
64
|
+
const :citations, T::Array[AnswerCitation]
|
|
65
|
+
const :cost_dollars, T.nilable(CostDollars)
|
|
66
|
+
|
|
67
|
+
def self.from_hash(hash)
|
|
68
|
+
sym = Helpers.symbolize_keys(hash)
|
|
69
|
+
new(
|
|
70
|
+
answer: sym[:answer],
|
|
71
|
+
citations: Array(sym[:citations]).map { AnswerCitation.from_hash(_1) },
|
|
72
|
+
cost_dollars: CostDollars.from_hash(sym[:costDollars])
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -8,7 +8,7 @@ module Exa
|
|
|
8
8
|
const :search_type, T.nilable(String)
|
|
9
9
|
const :results, T::Array[ResultWithContent]
|
|
10
10
|
const :context, T.nilable(String)
|
|
11
|
-
const :cost_dollars, T.nilable(Float)
|
|
11
|
+
const :cost_dollars, T.nilable(T.any(Float, T::Hash[Symbol, T.untyped]))
|
|
12
12
|
|
|
13
13
|
def self.from_hash(hash)
|
|
14
14
|
sym = Helpers.symbolize_keys(hash)
|
|
@@ -18,9 +18,22 @@ module Exa
|
|
|
18
18
|
search_type: sym[:searchType],
|
|
19
19
|
results: Array(sym[:results]).map { ResultWithContent.from_hash(_1) },
|
|
20
20
|
context: sym[:context],
|
|
21
|
-
cost_dollars: sym[:costDollars]
|
|
21
|
+
cost_dollars: normalize_cost(sym[:costDollars])
|
|
22
22
|
)
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
def self.normalize_cost(value)
|
|
26
|
+
case value
|
|
27
|
+
when nil
|
|
28
|
+
nil
|
|
29
|
+
when Numeric
|
|
30
|
+
value.to_f
|
|
31
|
+
when Hash
|
|
32
|
+
Exa::Responses::Helpers.symbolize_keys(value)
|
|
33
|
+
else
|
|
34
|
+
value
|
|
35
|
+
end
|
|
36
|
+
end
|
|
24
37
|
end
|
|
25
38
|
|
|
26
39
|
class FindSimilarResponse < T::Struct
|
data/lib/exa/responses.rb
CHANGED
|
@@ -10,4 +10,5 @@ require_relative "responses/webhook_response"
|
|
|
10
10
|
require_relative "responses/import_response"
|
|
11
11
|
require_relative "responses/webset_response"
|
|
12
12
|
require_relative "responses/research_response"
|
|
13
|
+
require_relative "responses/answer_response"
|
|
13
14
|
require_relative "responses/raw_response"
|
data/lib/exa/types/answer.rb
CHANGED
data/lib/exa/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: exa-ai-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vicente Reig Rincon de Arellano
|
|
@@ -51,6 +51,48 @@ dependencies:
|
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '1.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: thor
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.3'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.3'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: tty-table
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0.12'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0.12'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: pastel
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0.8'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0.8'
|
|
54
96
|
- !ruby/object:Gem::Dependency
|
|
55
97
|
name: minitest
|
|
56
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -93,17 +135,65 @@ dependencies:
|
|
|
93
135
|
- - "~>"
|
|
94
136
|
- !ruby/object:Gem::Version
|
|
95
137
|
version: '1.64'
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: aruba
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - "~>"
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '2.2'
|
|
145
|
+
type: :development
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - "~>"
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '2.2'
|
|
152
|
+
- !ruby/object:Gem::Dependency
|
|
153
|
+
name: webmock
|
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
|
155
|
+
requirements:
|
|
156
|
+
- - "~>"
|
|
157
|
+
- !ruby/object:Gem::Version
|
|
158
|
+
version: '3.23'
|
|
159
|
+
type: :development
|
|
160
|
+
prerelease: false
|
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - "~>"
|
|
164
|
+
- !ruby/object:Gem::Version
|
|
165
|
+
version: '3.23'
|
|
166
|
+
- !ruby/object:Gem::Dependency
|
|
167
|
+
name: webrick
|
|
168
|
+
requirement: !ruby/object:Gem::Requirement
|
|
169
|
+
requirements:
|
|
170
|
+
- - "~>"
|
|
171
|
+
- !ruby/object:Gem::Version
|
|
172
|
+
version: '1.8'
|
|
173
|
+
type: :development
|
|
174
|
+
prerelease: false
|
|
175
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
176
|
+
requirements:
|
|
177
|
+
- - "~>"
|
|
178
|
+
- !ruby/object:Gem::Version
|
|
179
|
+
version: '1.8'
|
|
96
180
|
description: Exa API client in Ruby, Sorbet-friendly and inspired by openai-ruby.
|
|
97
181
|
email:
|
|
98
182
|
- hey@vicente.services
|
|
99
|
-
executables:
|
|
183
|
+
executables:
|
|
184
|
+
- exa
|
|
100
185
|
extensions: []
|
|
101
186
|
extra_rdoc_files: []
|
|
102
187
|
files:
|
|
103
188
|
- CHANGELOG.md
|
|
104
189
|
- LICENSE
|
|
105
190
|
- README.md
|
|
191
|
+
- exe/exa
|
|
106
192
|
- lib/exa.rb
|
|
193
|
+
- lib/exa/cli.rb
|
|
194
|
+
- lib/exa/cli/account_resolver.rb
|
|
195
|
+
- lib/exa/cli/config_store.rb
|
|
196
|
+
- lib/exa/cli/root.rb
|
|
107
197
|
- lib/exa/client.rb
|
|
108
198
|
- lib/exa/errors.rb
|
|
109
199
|
- lib/exa/internal/transport/base_client.rb
|
|
@@ -122,6 +212,7 @@ files:
|
|
|
122
212
|
- lib/exa/resources/websets/items.rb
|
|
123
213
|
- lib/exa/resources/websets/monitors.rb
|
|
124
214
|
- lib/exa/responses.rb
|
|
215
|
+
- lib/exa/responses/answer_response.rb
|
|
125
216
|
- lib/exa/responses/contents_response.rb
|
|
126
217
|
- lib/exa/responses/event_response.rb
|
|
127
218
|
- lib/exa/responses/helpers.rb
|