exa-ai-ruby 1.1.2 → 1.2.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 +8 -0
- data/README.md +31 -0
- data/lib/exa/internal/transport/async_requester.rb +123 -0
- data/lib/exa/version.rb +1 -1
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79ae8c332333d78a85777c6661ee49c1d413048025d03258c22647287bd445fb
|
|
4
|
+
data.tar.gz: 938adda9c303fc20ec216c2ac70d4e0dde78934e7ec6ab4c3f1318900fcabeff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73a0621ead908309d13d516d74e5e720e8a01e5f09b9c4ee3f940a7af5fa090cbb40810f0273bd9208190f395371675168f005f50c24430c0c29b805a4dfdb40
|
|
7
|
+
data.tar.gz: a19fef60b65061fb8c450aa4c30dffb2e23dac495b6ef1db4c09a7a6c3f9fe489710cce167bc16279439e1853274b6cc3d4431127b9fef414dc677b29089762b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.0] - 2025-10-31
|
|
4
|
+
- Add `Exa::Internal::Transport::AsyncRequester`, enabling optional fiber-scheduler-friendly HTTP via the `async` ecosystem.
|
|
5
|
+
- Document async usage in the README with Gemfile requirements and concurrent request example.
|
|
6
|
+
- Cover the new requester with Minitest specs and wire optional development/test dependencies so contributors can run the async suite locally.
|
|
7
|
+
|
|
8
|
+
## [1.1.3] - 2025-10-30
|
|
9
|
+
- Extend compatibility down to Ruby 3.0.x by lowering the minimum required Ruby version.
|
|
10
|
+
|
|
3
11
|
## [1.1.2] - 2025-10-27
|
|
4
12
|
- Broaden support to Ruby 3.1+ by relaxing the gem's minimum Ruby requirement.
|
|
5
13
|
|
data/README.md
CHANGED
|
@@ -69,6 +69,37 @@ Runtime dependencies:
|
|
|
69
69
|
- `connection_pool` – `Net::HTTP` pooling in `PooledNetRequester`.
|
|
70
70
|
- `dspy-schema` – converts Sorbet types to JSON Schema (structured output support).
|
|
71
71
|
|
|
72
|
+
### Optional: Async transports
|
|
73
|
+
|
|
74
|
+
To integrate with Ruby’s `async` scheduler, add the optional dependencies and inject the provided requester:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# Gemfile
|
|
78
|
+
gem "async", "~> 2.6"
|
|
79
|
+
gem "async-http", "~> 0.92"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
require "async"
|
|
84
|
+
require "exa/internal/transport/async_requester"
|
|
85
|
+
require "exa"
|
|
86
|
+
|
|
87
|
+
Async do
|
|
88
|
+
requester = Exa::Internal::Transport::AsyncRequester.new
|
|
89
|
+
client = Exa::Client.new(api_key: ENV.fetch("EXA_API_KEY"), requester: requester)
|
|
90
|
+
|
|
91
|
+
search_task = Async { client.search.search(query: "autonomous robotics", num_results: 3) }
|
|
92
|
+
research_task = Async { client.research.create(instructions: "Track major AI policy updates.") }
|
|
93
|
+
|
|
94
|
+
puts search_task.wait.results.first.title
|
|
95
|
+
puts research_task.wait.id
|
|
96
|
+
ensure
|
|
97
|
+
requester.close
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The async requester preserves the same typed resources and streaming helpers, so switching between synchronous and asynchronous transports is a single constructor change.
|
|
102
|
+
|
|
72
103
|
Set the API key via `EXA_API_KEY` or pass `api_key:` when instantiating `Exa::Client`.
|
|
73
104
|
|
|
74
105
|
If you are building automation that calls this README (e.g., using `curl`/`wget` or a retrieval plug‑in), fetch the raw file from GitHub: `https://raw.githubusercontent.com/vicentereig/exa-ruby/main/README.md`.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "async"
|
|
5
|
+
require "async/http/internet"
|
|
6
|
+
rescue LoadError => e
|
|
7
|
+
raise LoadError, "Install the `async` and `async-http` gems to use AsyncRequester (caused by #{e.message})"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
require "exa/errors"
|
|
11
|
+
require_relative "../util"
|
|
12
|
+
|
|
13
|
+
module Exa
|
|
14
|
+
module Internal
|
|
15
|
+
module Transport
|
|
16
|
+
class AsyncRequester
|
|
17
|
+
# Wraps an Async::HTTP::Response to mimic Net::HTTP's header API.
|
|
18
|
+
class ResponseAdapter
|
|
19
|
+
def initialize(response)
|
|
20
|
+
@response = response
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def each_header
|
|
24
|
+
return enum_for(__method__) unless block_given?
|
|
25
|
+
@response.headers.each do |key, value|
|
|
26
|
+
Array(value).each { |v| yield(key, v) }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def [](key)
|
|
31
|
+
value = @response.headers[key]
|
|
32
|
+
value.is_a?(Array) ? value.first : value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def finish
|
|
36
|
+
@response.finish
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
attr_reader :internet
|
|
41
|
+
|
|
42
|
+
def initialize(internet: nil)
|
|
43
|
+
@internet = internet || Async::HTTP::Internet.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def execute(request)
|
|
47
|
+
task = Async::Task.current?
|
|
48
|
+
unless task
|
|
49
|
+
raise Exa::Errors::ConfigurationError,
|
|
50
|
+
"AsyncRequester must run inside an Async scheduler (wrap calls in `Async do ... end`)."
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
deadline = request.fetch(:deadline)
|
|
54
|
+
response = with_timeout(deadline, request[:url]) do
|
|
55
|
+
internet.call(
|
|
56
|
+
request.fetch(:method).to_s.upcase,
|
|
57
|
+
request.fetch(:url).to_s,
|
|
58
|
+
request.fetch(:headers),
|
|
59
|
+
request[:body]
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
adapter = ResponseAdapter.new(response)
|
|
64
|
+
|
|
65
|
+
body_enum = build_body_enum(response: response, deadline: deadline, url: request.fetch(:url).to_s)
|
|
66
|
+
|
|
67
|
+
[Integer(response.status), adapter, body_enum]
|
|
68
|
+
rescue Async::TimeoutError
|
|
69
|
+
raise Exa::Errors::APITimeoutError.new("Request timed out", url: request[:url])
|
|
70
|
+
rescue Exa::Errors::APIError, Exa::Errors::ConfigurationError
|
|
71
|
+
raise
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
raise Exa::Errors::APIConnectionError.new(e.message, url: request[:url])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def close
|
|
77
|
+
internet.close
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def build_body_enum(response:, deadline:, url:)
|
|
83
|
+
body = response.body
|
|
84
|
+
Exa::Internal::Util.fused_enum(
|
|
85
|
+
Enumerator.new do |y|
|
|
86
|
+
begin
|
|
87
|
+
loop do
|
|
88
|
+
chunk = read_chunk(body: body, deadline: deadline, url: url)
|
|
89
|
+
break if chunk.nil?
|
|
90
|
+
y << chunk
|
|
91
|
+
end
|
|
92
|
+
ensure
|
|
93
|
+
response.finish
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
) { response.finish }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def read_chunk(body:, deadline:, url:)
|
|
100
|
+
remaining = remaining(deadline)
|
|
101
|
+
return nil if remaining <= 0
|
|
102
|
+
|
|
103
|
+
Async::Task.current.with_timeout(remaining) do
|
|
104
|
+
body.read
|
|
105
|
+
end
|
|
106
|
+
rescue Async::TimeoutError
|
|
107
|
+
raise Exa::Errors::APITimeoutError.new("Request timed out", url: url)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def with_timeout(deadline, url)
|
|
111
|
+
remaining = remaining(deadline)
|
|
112
|
+
raise Exa::Errors::APITimeoutError.new("Request timed out", url: url) if remaining <= 0
|
|
113
|
+
|
|
114
|
+
Async::Task.current.with_timeout(remaining) { yield }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def remaining(deadline)
|
|
118
|
+
deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vicente Reig Rincon de Arellano
|
|
@@ -44,6 +44,9 @@ dependencies:
|
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
46
|
version: '1.0'
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: 1.0.1
|
|
47
50
|
type: :runtime
|
|
48
51
|
prerelease: false
|
|
49
52
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -51,6 +54,9 @@ dependencies:
|
|
|
51
54
|
- - "~>"
|
|
52
55
|
- !ruby/object:Gem::Version
|
|
53
56
|
version: '1.0'
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: 1.0.1
|
|
54
60
|
- !ruby/object:Gem::Dependency
|
|
55
61
|
name: thor
|
|
56
62
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -197,6 +203,7 @@ files:
|
|
|
197
203
|
- lib/exa/cli/root.rb
|
|
198
204
|
- lib/exa/client.rb
|
|
199
205
|
- lib/exa/errors.rb
|
|
206
|
+
- lib/exa/internal/transport/async_requester.rb
|
|
200
207
|
- lib/exa/internal/transport/base_client.rb
|
|
201
208
|
- lib/exa/internal/transport/pooled_net_requester.rb
|
|
202
209
|
- lib/exa/internal/transport/stream.rb
|
|
@@ -249,7 +256,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
249
256
|
requirements:
|
|
250
257
|
- - ">="
|
|
251
258
|
- !ruby/object:Gem::Version
|
|
252
|
-
version:
|
|
259
|
+
version: 3.0.0
|
|
253
260
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
254
261
|
requirements:
|
|
255
262
|
- - ">="
|