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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db2209bdedef9b83244089dc9741f0f5efb80c97b6dcb5a35d84bdbfa5a75578
4
- data.tar.gz: 44d3a68b788de10a8515524efb56c5cad08e3ab0a5f9278f4d0c32a55471100c
3
+ metadata.gz: 79ae8c332333d78a85777c6661ee49c1d413048025d03258c22647287bd445fb
4
+ data.tar.gz: 938adda9c303fc20ec216c2ac70d4e0dde78934e7ec6ab4c3f1318900fcabeff
5
5
  SHA512:
6
- metadata.gz: dc728d52cb343576e48632e765864e47f5b800662d15cc9748e2c438e9b0f93c1b71bb1f884075337117fcfa755d4d3fca702c42093157783877bf0adf5d3677
7
- data.tar.gz: 938036e4dd8429695ec519870cd3edb121c4b113c3127f557a790a02246f71a51335802f963330f7a83360236b9931dee8adc3c8c9724eb40bb11a49de4008dc
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Exa
4
- VERSION = "1.1.2"
4
+ VERSION = "1.2.0"
5
5
  end
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.1.2
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: '3.1'
259
+ version: 3.0.0
253
260
  required_rubygems_version: !ruby/object:Gem::Requirement
254
261
  requirements:
255
262
  - - ">="