exa-rb 1.0.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +286 -0
- data/bin/exa +164 -0
- data/exa-rb.gemspec +41 -0
- data/lib/exa/answer_options.rb +29 -0
- data/lib/exa/answer_request.rb +26 -0
- data/lib/exa/answer_result.rb +29 -0
- data/lib/exa/contents_options.rb +56 -0
- data/lib/exa/contents_request.rb +28 -0
- data/lib/exa/contents_result.rb +30 -0
- data/lib/exa/error_result.rb +46 -0
- data/lib/exa/find_similar_options.rb +66 -0
- data/lib/exa/find_similar_request.rb +26 -0
- data/lib/exa/find_similar_result.rb +32 -0
- data/lib/exa/helpers.rb +8 -0
- data/lib/exa/module_methods.rb +31 -0
- data/lib/exa/request.rb +41 -0
- data/lib/exa/response_methods.rb +12 -0
- data/lib/exa/search_options.rb +74 -0
- data/lib/exa/search_request.rb +26 -0
- data/lib/exa/search_result.rb +42 -0
- data/lib/exa/version.rb +3 -0
- data/lib/exa.rb +34 -0
- metadata +140 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ba7c3d6444e98f398c76d223c995409c7b835df4c42c543c0ce67b7e1b059137
|
|
4
|
+
data.tar.gz: c5480dc30c83a9d4510261dbfbbd8d3b5fbd28be6adaf4a109b8a289b7f37799
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f8742c3096bac80fbd171db29c67c5ac2fc40bd181f27fcc6de5135e7e1a8cca005b63f26f96c799130a75fc01421b8d2026adc81c932a10ef8bfe2a2a33f9c8
|
|
7
|
+
data.tar.gz: 9a48a2842a556c4407cb826e5748eff67b8d4dc83c8657d40a7c9bbd094e3e87edfa67550988c2931117da2d68e6fd20a2f364fbf4a5c627b2a3d587cc558e48
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Endless International
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# Exa
|
|
2
|
+
|
|
3
|
+
The **Exa** gem provides a Ruby interface to the [Exa API](https://exa.ai), enabling powerful neural and keyword-based web search with content retrieval capabilities. Exa goes beyond traditional keyword matching to understand semantic meaning and context, making it particularly useful for applications that need intelligent search, including those using Large Language Models.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
require 'exa'
|
|
7
|
+
|
|
8
|
+
Exa.api_key ENV[ 'EXA_API_KEY' ]
|
|
9
|
+
|
|
10
|
+
response = Exa.search( 'best practices for Ruby API design' )
|
|
11
|
+
if response.success?
|
|
12
|
+
response.result.results.each do | result |
|
|
13
|
+
puts result.title
|
|
14
|
+
puts result.url
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Table of Contents
|
|
22
|
+
|
|
23
|
+
- [Installation](#installation)
|
|
24
|
+
- [Command Line](#command-line)
|
|
25
|
+
- [Quick Start](#quick-start)
|
|
26
|
+
- [Endpoints](#endpoints)
|
|
27
|
+
- [Search](#search)
|
|
28
|
+
- [Find Similar](#find-similar)
|
|
29
|
+
- [Answer](#answer)
|
|
30
|
+
- [Contents](#contents)
|
|
31
|
+
- [Responses and Errors](#responses-and-errors)
|
|
32
|
+
- [Connections](#connections)
|
|
33
|
+
- [License](#license)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
Add this line to your application's Gemfile:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
gem 'exa-rb'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then execute:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
bundle install
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or install it directly:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
gem install exa-rb
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Command Line
|
|
58
|
+
|
|
59
|
+
The gem includes an `exa` command for quick searches from the terminal:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
exa search "Ruby programming best practices"
|
|
63
|
+
exa answer "What is the Ruby programming language?"
|
|
64
|
+
exa similar https://www.ruby-lang.org/en/
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Set your API key via environment variable:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
export EXA_API_KEY=your_api_key
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Quick Start
|
|
74
|
+
|
|
75
|
+
The simplest way to use the gem is through the module-level convenience methods. Set your API key once, then call any endpoint:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
require 'exa'
|
|
79
|
+
|
|
80
|
+
Exa.api_key ENV[ 'EXA_API_KEY' ]
|
|
81
|
+
|
|
82
|
+
response = Exa.search( 'machine learning tutorials' )
|
|
83
|
+
if response.success?
|
|
84
|
+
response.result.results.each do | result |
|
|
85
|
+
puts result.title
|
|
86
|
+
puts result.url
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For more control, instantiate request objects directly. This allows you to configure options using a block-based DSL and reuse request instances:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
request = Exa::SearchRequest.new( api_key: ENV[ 'EXA_API_KEY' ] )
|
|
95
|
+
|
|
96
|
+
options = Exa::SearchOptions.build do
|
|
97
|
+
num_results 10
|
|
98
|
+
type :neural
|
|
99
|
+
contents do
|
|
100
|
+
text { max_characters 1000 }
|
|
101
|
+
highlights { num_sentences 2 }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
response = request.submit( 'Ruby web frameworks', options )
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Endpoints
|
|
111
|
+
|
|
112
|
+
### Search
|
|
113
|
+
|
|
114
|
+
The search endpoint performs web searches using neural or keyword-based methods and returns relevant results with optional content retrieval.
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
options = Exa::SearchOptions.build do
|
|
118
|
+
num_results 5
|
|
119
|
+
type :neural
|
|
120
|
+
include_domains [ 'github.com', 'stackoverflow.com' ]
|
|
121
|
+
contents do
|
|
122
|
+
summary { query 'Summarize the key points' }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
response = Exa.search( 'Ruby dependency injection patterns', options )
|
|
127
|
+
|
|
128
|
+
if response.success?
|
|
129
|
+
response.result.results.each do | result |
|
|
130
|
+
puts result.title
|
|
131
|
+
puts result.url
|
|
132
|
+
puts result.summary
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
For complete documentation of all search options and response fields, see [Search Documentation](readme/search.md).
|
|
138
|
+
|
|
139
|
+
### Find Similar
|
|
140
|
+
|
|
141
|
+
The find similar endpoint finds pages that are similar to a given URL:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
options = Exa::FindSimilarOptions.build do
|
|
145
|
+
num_results 5
|
|
146
|
+
exclude_source_domain true
|
|
147
|
+
contents do
|
|
148
|
+
summary { query 'Summarize the content' }
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
response = Exa.find_similar( 'https://www.ruby-lang.org/en/', options )
|
|
153
|
+
|
|
154
|
+
if response.success?
|
|
155
|
+
response.result.results.each do | result |
|
|
156
|
+
puts result.title
|
|
157
|
+
puts result.url
|
|
158
|
+
puts result.summary
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
For complete documentation of all find similar options and response fields, see [Find Similar Documentation](readme/find_similar.md).
|
|
164
|
+
|
|
165
|
+
### Answer
|
|
166
|
+
|
|
167
|
+
The answer endpoint provides AI-generated answers to questions using web sources:
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
response = Exa.answer( 'What is the Ruby programming language?' )
|
|
171
|
+
|
|
172
|
+
if response.success?
|
|
173
|
+
puts response.result.answer
|
|
174
|
+
|
|
175
|
+
puts "\nCitations:"
|
|
176
|
+
response.result.citations.each do | citation |
|
|
177
|
+
puts "- #{ citation.title }: #{ citation.url }"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
To include full text from citations:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
options = Exa::AnswerOptions.build do
|
|
186
|
+
text true
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
response = Exa.answer( 'How does garbage collection work in Ruby?', options )
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
For complete documentation of all answer options and response fields, see [Answer Documentation](readme/answer.md).
|
|
193
|
+
|
|
194
|
+
### Contents
|
|
195
|
+
|
|
196
|
+
The contents endpoint retrieves full page content, summaries, and highlights for a list of URLs:
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
urls = [
|
|
200
|
+
'https://ruby-doc.org/core/Array.html',
|
|
201
|
+
'https://ruby-doc.org/core/Hash.html'
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
options = Exa::ContentsOptions.build do
|
|
205
|
+
text { max_characters 2000 }
|
|
206
|
+
highlights do
|
|
207
|
+
num_sentences 3
|
|
208
|
+
highlights_per_url 5
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
response = Exa.contents( urls, options )
|
|
213
|
+
|
|
214
|
+
if response.success?
|
|
215
|
+
response.result.results.each do | result |
|
|
216
|
+
puts result.title
|
|
217
|
+
puts result.text
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
For complete documentation of all contents options and response fields, see [Contents Documentation](readme/contents.md).
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Responses and Errors
|
|
227
|
+
|
|
228
|
+
All request methods return a `Faraday::Response` object. Check `response.success?` to determine if the HTTP request succeeded. When successful, `response.result` contains the parsed result object specific to the endpoint.
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
response = Exa.search( query, options )
|
|
232
|
+
|
|
233
|
+
if response.success?
|
|
234
|
+
result = response.result
|
|
235
|
+
result.results.each do | item |
|
|
236
|
+
puts item.title
|
|
237
|
+
end
|
|
238
|
+
else
|
|
239
|
+
error = response.result
|
|
240
|
+
puts error.error_type # :authentication_error, :rate_limit_error, etc.
|
|
241
|
+
puts error.error_description # human-readable message
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The gem maps HTTP status codes to error types:
|
|
246
|
+
|
|
247
|
+
| Status | Error Type | Description |
|
|
248
|
+
|--------|------------|-------------|
|
|
249
|
+
| 400 | `:invalid_request_error` | Invalid request parameters or malformed JSON |
|
|
250
|
+
| 401 | `:authentication_error` | Missing or invalid API key |
|
|
251
|
+
| 403 | `:forbidden_error` | Insufficient permissions or rate limit exceeded |
|
|
252
|
+
| 404 | `:not_found_error` | The requested resource was not found |
|
|
253
|
+
| 409 | `:conflict_error` | Resource already exists |
|
|
254
|
+
| 429 | `:rate_limit_error` | Rate limit exceeded |
|
|
255
|
+
| 500-599 | `:server_error` | A server error occurred |
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Connections
|
|
260
|
+
|
|
261
|
+
The gem uses Faraday for HTTP requests, which means you can customize the connection configuration. To use a custom connection:
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
connection = Faraday.new do | faraday |
|
|
265
|
+
faraday.request :json
|
|
266
|
+
faraday.response :logger
|
|
267
|
+
faraday.adapter :net_http
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
Exa.connection connection
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Or pass it directly to a request:
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
request = Exa::SearchRequest.new(
|
|
277
|
+
api_key: ENV[ 'EXA_API_KEY' ],
|
|
278
|
+
connection: connection
|
|
279
|
+
)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## License
|
|
285
|
+
|
|
286
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/bin/exa
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'exa'
|
|
4
|
+
|
|
5
|
+
COMMANDS = %w[ search answer similar help ]
|
|
6
|
+
|
|
7
|
+
def usage
|
|
8
|
+
puts "Usage: exa <command> [arguments]"
|
|
9
|
+
puts
|
|
10
|
+
puts "Commands:"
|
|
11
|
+
puts " search <query> Search the web and show results with summaries"
|
|
12
|
+
puts " answer <question> Get an AI-generated answer with citations"
|
|
13
|
+
puts " similar <url> Find pages similar to a URL"
|
|
14
|
+
puts " help Show this help message"
|
|
15
|
+
puts
|
|
16
|
+
puts "Environment:"
|
|
17
|
+
puts " EXA_API_KEY Your Exa API key (required)"
|
|
18
|
+
puts
|
|
19
|
+
puts "Options:"
|
|
20
|
+
puts " -n, --num <n> Number of results (default: 5)"
|
|
21
|
+
puts
|
|
22
|
+
puts "Examples:"
|
|
23
|
+
puts " exa search \"Ruby programming best practices\""
|
|
24
|
+
puts " exa answer \"What is the Ruby programming language?\""
|
|
25
|
+
puts " exa similar https://www.ruby-lang.org/en/"
|
|
26
|
+
puts " exa search -n 10 \"machine learning tutorials\""
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def check_api_key!
|
|
30
|
+
unless ENV[ 'EXA_API_KEY' ]
|
|
31
|
+
$stderr.puts "Error: An EXA_API_KEY environment variable is required."
|
|
32
|
+
exit 1
|
|
33
|
+
end
|
|
34
|
+
Exa.api_key ENV[ 'EXA_API_KEY' ]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parse_options( args )
|
|
38
|
+
options = { num_results: 5 }
|
|
39
|
+
|
|
40
|
+
while args.first&.start_with?( '-' )
|
|
41
|
+
case args.shift
|
|
42
|
+
when '-n', '--num'
|
|
43
|
+
options[ :num_results ] = args.shift.to_i
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
options
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def cmd_search( args )
|
|
51
|
+
options = parse_options( args )
|
|
52
|
+
query = args.join( ' ' )
|
|
53
|
+
|
|
54
|
+
if query.empty?
|
|
55
|
+
$stderr.puts "Error: search query required"
|
|
56
|
+
$stderr.puts "Usage: exa search <query>"
|
|
57
|
+
exit 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
check_api_key!
|
|
61
|
+
|
|
62
|
+
search_options = Exa::SearchOptions.build do
|
|
63
|
+
num_results options[ :num_results ]
|
|
64
|
+
contents do
|
|
65
|
+
summary { query 'Summarize in a single paragraph' }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
response = Exa.search( query, search_options )
|
|
70
|
+
|
|
71
|
+
unless response.success?
|
|
72
|
+
$stderr.puts "Error: #{ response.result.error_description }"
|
|
73
|
+
exit 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
response.result.results.each_with_index do | result, index |
|
|
77
|
+
puts if index > 0
|
|
78
|
+
puts result.title
|
|
79
|
+
puts result.url
|
|
80
|
+
puts result.summary if result.summary
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def cmd_answer( args )
|
|
85
|
+
options = parse_options( args )
|
|
86
|
+
question = args.join( ' ' )
|
|
87
|
+
|
|
88
|
+
if question.empty?
|
|
89
|
+
$stderr.puts "Error: question required"
|
|
90
|
+
$stderr.puts "Usage: exa answer <question>"
|
|
91
|
+
exit 1
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
check_api_key!
|
|
95
|
+
|
|
96
|
+
response = Exa.answer( question )
|
|
97
|
+
|
|
98
|
+
unless response.success?
|
|
99
|
+
$stderr.puts "Error: #{ response.result.error_description }"
|
|
100
|
+
exit 1
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
result = response.result
|
|
104
|
+
puts result.answer
|
|
105
|
+
puts
|
|
106
|
+
puts "Sources:"
|
|
107
|
+
result.citations.each do | citation |
|
|
108
|
+
puts " #{ citation.title }"
|
|
109
|
+
puts " #{ citation.url }"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def cmd_similar( args )
|
|
114
|
+
options = parse_options( args )
|
|
115
|
+
url = args.first
|
|
116
|
+
|
|
117
|
+
if url.nil? || url.empty?
|
|
118
|
+
$stderr.puts "Error: URL required"
|
|
119
|
+
$stderr.puts "Usage: exa similar <url>"
|
|
120
|
+
exit 1
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
check_api_key!
|
|
124
|
+
|
|
125
|
+
similar_options = Exa::FindSimilarOptions.build do
|
|
126
|
+
num_results options[ :num_results ]
|
|
127
|
+
exclude_source_domain true
|
|
128
|
+
contents do
|
|
129
|
+
summary { query 'Summarize in a single paragraph' }
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
response = Exa.find_similar( url, similar_options )
|
|
134
|
+
|
|
135
|
+
unless response.success?
|
|
136
|
+
$stderr.puts "Error: #{ response.result.error_description }"
|
|
137
|
+
exit 1
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
response.result.results.each_with_index do | result, index |
|
|
141
|
+
puts if index > 0
|
|
142
|
+
puts result.title
|
|
143
|
+
puts result.url
|
|
144
|
+
puts result.summary if result.summary
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Main
|
|
149
|
+
command = ARGV.shift
|
|
150
|
+
|
|
151
|
+
case command
|
|
152
|
+
when 'search'
|
|
153
|
+
cmd_search( ARGV )
|
|
154
|
+
when 'answer'
|
|
155
|
+
cmd_answer( ARGV )
|
|
156
|
+
when 'similar'
|
|
157
|
+
cmd_similar( ARGV )
|
|
158
|
+
when 'help', '-h', '--help', nil
|
|
159
|
+
usage
|
|
160
|
+
else
|
|
161
|
+
$stderr.puts "Unknown command: #{ command }"
|
|
162
|
+
$stderr.puts "Run 'exa help' for usage"
|
|
163
|
+
exit 1
|
|
164
|
+
end
|
data/exa-rb.gemspec
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require_relative 'lib/exa/version'
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do | spec |
|
|
4
|
+
|
|
5
|
+
spec.name = 'exa-rb'
|
|
6
|
+
spec.version = Exa::VERSION
|
|
7
|
+
spec.authors = [ 'Kristoph Cichocki-Romanov' ]
|
|
8
|
+
spec.email = [ 'rubygems.org@kristoph.net' ]
|
|
9
|
+
|
|
10
|
+
spec.summary =
|
|
11
|
+
"The Exa gem implements a lightweight interface to the Exa.ai API for neural " \
|
|
12
|
+
"and keyword-based web search with content retrieval capabilities."
|
|
13
|
+
spec.description =
|
|
14
|
+
"The Exa gem implements a lightweight interface to the Exa.ai API. Exa provides " \
|
|
15
|
+
"powerful neural search capabilities that go beyond traditional keyword matching " \
|
|
16
|
+
"to understand semantic meaning and context.\n" \
|
|
17
|
+
"\n" \
|
|
18
|
+
"This gem supports search, find similar, answer, and contents endpoints. It " \
|
|
19
|
+
"includes both a Ruby API and a command-line interface (exa) for quick searches " \
|
|
20
|
+
"from the terminal."
|
|
21
|
+
spec.license = 'MIT'
|
|
22
|
+
spec.homepage = 'https://github.com/EndlessInternational/exa-rb'
|
|
23
|
+
spec.metadata = {
|
|
24
|
+
'source_code_uri' => 'https://github.com/EndlessInternational/exa-rb',
|
|
25
|
+
'bug_tracker_uri' => 'https://github.com/EndlessInternational/exa-rb/issues',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
spec.required_ruby_version = '>= 3.0'
|
|
29
|
+
spec.files = Dir[ "lib/**/*.rb", "bin/exa", "LICENSE", "README.md", "exa-rb.gemspec" ]
|
|
30
|
+
spec.bindir = 'bin'
|
|
31
|
+
spec.executables = [ 'exa' ]
|
|
32
|
+
spec.require_paths = [ "lib" ]
|
|
33
|
+
|
|
34
|
+
spec.add_runtime_dependency 'faraday', '~> 2'
|
|
35
|
+
spec.add_runtime_dependency 'dynamicschema', '~> 2'
|
|
36
|
+
|
|
37
|
+
spec.add_development_dependency 'minitest', '~> 5.25'
|
|
38
|
+
spec.add_development_dependency 'debug', '~> 1.9'
|
|
39
|
+
spec.add_development_dependency 'vcr', '~> 6.3'
|
|
40
|
+
|
|
41
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Exa
|
|
2
|
+
class AnswerOptions
|
|
3
|
+
include DynamicSchema::Definable
|
|
4
|
+
include Helpers
|
|
5
|
+
|
|
6
|
+
schema do
|
|
7
|
+
text [ TrueClass, FalseClass ]
|
|
8
|
+
stream [ TrueClass, FalseClass ]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.build( options = nil, &block )
|
|
12
|
+
new( api_options: builder.build( options, &block ) )
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.build!( options = nil, &block )
|
|
16
|
+
new( api_options: builder.build!( options, &block ) )
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize( options = {}, api_options: nil )
|
|
20
|
+
@options = self.class.builder.build( options || {} )
|
|
21
|
+
@options = api_options.merge( @options ) if api_options
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
@options.to_h
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Exa
|
|
2
|
+
class AnswerRequest < Request
|
|
3
|
+
|
|
4
|
+
def submit( query, options = nil, &block )
|
|
5
|
+
if options
|
|
6
|
+
options = options.is_a?( AnswerOptions ) ? options : AnswerOptions.build!( options.to_h )
|
|
7
|
+
options = options.to_h
|
|
8
|
+
else
|
|
9
|
+
options = {}
|
|
10
|
+
end
|
|
11
|
+
options[ :query ] = query.to_s
|
|
12
|
+
|
|
13
|
+
response = post( "#{ BASE_URI }/answer", options, &block )
|
|
14
|
+
attributes = ( JSON.parse( response.body, symbolize_names: true ) rescue nil )
|
|
15
|
+
|
|
16
|
+
result = if response.success?
|
|
17
|
+
AnswerResult.new( attributes )
|
|
18
|
+
else
|
|
19
|
+
ErrorResult.new( response.status, attributes )
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
ResponseMethods.install( response, result )
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Exa
|
|
2
|
+
|
|
3
|
+
CitationSchema = DynamicSchema::Struct.define do
|
|
4
|
+
id String
|
|
5
|
+
url String
|
|
6
|
+
title String
|
|
7
|
+
author String
|
|
8
|
+
published_date String, as: :publishedDate
|
|
9
|
+
text String
|
|
10
|
+
image String
|
|
11
|
+
favicon String
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Citation < CitationSchema
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
AnswerResultSchema = DynamicSchema::Struct.define do
|
|
18
|
+
request_id String, as: :requestId
|
|
19
|
+
answer String
|
|
20
|
+
citations Citation, array: true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class AnswerResult < AnswerResultSchema
|
|
24
|
+
def success?
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Exa
|
|
2
|
+
class ContentsOptions
|
|
3
|
+
include DynamicSchema::Definable
|
|
4
|
+
include Helpers
|
|
5
|
+
|
|
6
|
+
LIVECRAWL_OPTIONS = [ :never, :fallback, :always ]
|
|
7
|
+
|
|
8
|
+
schema do
|
|
9
|
+
|
|
10
|
+
# text retrieval
|
|
11
|
+
text do
|
|
12
|
+
max_characters Integer, as: :maxCharacters
|
|
13
|
+
include_html_tags [ TrueClass, FalseClass ], as: :includeHtmlTags
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# highlights
|
|
17
|
+
highlights do
|
|
18
|
+
num_sentences Integer, as: :numSentences
|
|
19
|
+
highlights_per_url Integer, as: :highlightsPerUrl
|
|
20
|
+
query String
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# summary
|
|
24
|
+
summary do
|
|
25
|
+
query String
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# crawling control
|
|
29
|
+
livecrawl Symbol, in: LIVECRAWL_OPTIONS
|
|
30
|
+
livecrawl_timeout Integer, as: :livecrawlTimeout
|
|
31
|
+
|
|
32
|
+
# subpages
|
|
33
|
+
subpages Integer
|
|
34
|
+
subpage_target String, as: :subpageTarget
|
|
35
|
+
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.build( options = nil, &block )
|
|
39
|
+
new( api_options: builder.build( options, &block ) )
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.build!( options = nil, &block )
|
|
43
|
+
new( api_options: builder.build!( options, &block ) )
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def initialize( options = {}, api_options: nil )
|
|
47
|
+
@options = self.class.builder.build( options || {} )
|
|
48
|
+
@options = api_options.merge( @options ) if api_options
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_h
|
|
52
|
+
@options.to_h
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Exa
|
|
2
|
+
class ContentsRequest < Request
|
|
3
|
+
|
|
4
|
+
def submit( urls, options = nil, &block )
|
|
5
|
+
urls = Array( urls ).map( &:to_s )
|
|
6
|
+
|
|
7
|
+
if options
|
|
8
|
+
options = options.is_a?( ContentsOptions ) ? options : ContentsOptions.build!( options.to_h )
|
|
9
|
+
options = options.to_h
|
|
10
|
+
else
|
|
11
|
+
options = {}
|
|
12
|
+
end
|
|
13
|
+
options[ :urls ] = urls
|
|
14
|
+
|
|
15
|
+
response = post( "#{ BASE_URI }/contents", options, &block )
|
|
16
|
+
attributes = ( JSON.parse( response.body, symbolize_names: true ) rescue nil )
|
|
17
|
+
|
|
18
|
+
result = if response.success?
|
|
19
|
+
ContentsResult.new( attributes )
|
|
20
|
+
else
|
|
21
|
+
ErrorResult.new( response.status, attributes )
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
ResponseMethods.install( response, result )
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Exa
|
|
2
|
+
|
|
3
|
+
ContentsResultItemSchema = DynamicSchema::Struct.define do
|
|
4
|
+
id String
|
|
5
|
+
url String
|
|
6
|
+
title String
|
|
7
|
+
author String
|
|
8
|
+
text String
|
|
9
|
+
highlights String, array: true
|
|
10
|
+
highlight_scores Float, array: true, as: :highlightScores
|
|
11
|
+
summary String
|
|
12
|
+
image String
|
|
13
|
+
favicon String
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class ContentsResultItem < ContentsResultItemSchema
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
ContentsResultSchema = DynamicSchema::Struct.define do
|
|
20
|
+
request_id String, as: :requestId
|
|
21
|
+
results ContentsResultItem, array: true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class ContentsResult < ContentsResultSchema
|
|
25
|
+
def success?
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Exa
|
|
2
|
+
class ErrorResult
|
|
3
|
+
|
|
4
|
+
attr_reader :error_type, :error_description
|
|
5
|
+
|
|
6
|
+
def initialize( status_code, attributes = nil )
|
|
7
|
+
@error_type, @error_description = status_code_to_error( status_code )
|
|
8
|
+
@error_description = attributes[ :error ] if attributes&.respond_to?( :[] ) && attributes[ :error ]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def status_code_to_error( status_code )
|
|
14
|
+
case status_code
|
|
15
|
+
when 200
|
|
16
|
+
[ :unexpected_error,
|
|
17
|
+
"The response was successful but it did not include a valid payload." ]
|
|
18
|
+
when 400
|
|
19
|
+
[ :invalid_request_error,
|
|
20
|
+
"Invalid request parameters, malformed JSON, or missing required fields." ]
|
|
21
|
+
when 401
|
|
22
|
+
[ :authentication_error,
|
|
23
|
+
"Missing or invalid API key." ]
|
|
24
|
+
when 403
|
|
25
|
+
[ :forbidden_error,
|
|
26
|
+
"Valid API key but insufficient permissions or rate limit exceeded." ]
|
|
27
|
+
when 404
|
|
28
|
+
[ :not_found_error,
|
|
29
|
+
"The requested resource was not found." ]
|
|
30
|
+
when 409
|
|
31
|
+
[ :conflict_error,
|
|
32
|
+
"Resource already exists." ]
|
|
33
|
+
when 429
|
|
34
|
+
[ :rate_limit_error,
|
|
35
|
+
"Rate limit exceeded." ]
|
|
36
|
+
when 500..599
|
|
37
|
+
[ :server_error,
|
|
38
|
+
"An error occurred on the Exa servers." ]
|
|
39
|
+
else
|
|
40
|
+
[ :unknown_error,
|
|
41
|
+
"The Exa service returned an unexpected status code: '#{ status_code }'." ]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module Exa
|
|
2
|
+
class FindSimilarOptions
|
|
3
|
+
include DynamicSchema::Definable
|
|
4
|
+
include Helpers
|
|
5
|
+
|
|
6
|
+
LIVECRAWL_OPTIONS = [ :never, :fallback, :always ]
|
|
7
|
+
|
|
8
|
+
schema do
|
|
9
|
+
|
|
10
|
+
# result control
|
|
11
|
+
num_results Integer, as: :numResults
|
|
12
|
+
exclude_source_domain [ TrueClass, FalseClass ], as: :excludeSourceDomain
|
|
13
|
+
|
|
14
|
+
# domain filtering
|
|
15
|
+
include_domains String, array: true, as: :includeDomains
|
|
16
|
+
exclude_domains String, array: true, as: :excludeDomains
|
|
17
|
+
|
|
18
|
+
# date filtering
|
|
19
|
+
start_crawl_date String, as: :startCrawlDate
|
|
20
|
+
end_crawl_date String, as: :endCrawlDate
|
|
21
|
+
start_published_date String, as: :startPublishedDate
|
|
22
|
+
end_published_date String, as: :endPublishedDate
|
|
23
|
+
|
|
24
|
+
# content filtering
|
|
25
|
+
include_text String, array: true, as: :includeText
|
|
26
|
+
exclude_text String, array: true, as: :excludeText
|
|
27
|
+
|
|
28
|
+
# content retrieval options
|
|
29
|
+
contents do
|
|
30
|
+
text do
|
|
31
|
+
max_characters Integer, as: :maxCharacters
|
|
32
|
+
include_html_tags [ TrueClass, FalseClass ], as: :includeHtmlTags
|
|
33
|
+
end
|
|
34
|
+
highlights do
|
|
35
|
+
num_sentences Integer, as: :numSentences
|
|
36
|
+
highlights_per_url Integer, as: :highlightsPerUrl
|
|
37
|
+
query String
|
|
38
|
+
end
|
|
39
|
+
summary do
|
|
40
|
+
query String
|
|
41
|
+
end
|
|
42
|
+
livecrawl Symbol, in: LIVECRAWL_OPTIONS
|
|
43
|
+
livecrawl_timeout Integer, as: :livecrawlTimeout
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.build( options = nil, &block )
|
|
49
|
+
new( api_options: builder.build( options, &block ) )
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.build!( options = nil, &block )
|
|
53
|
+
new( api_options: builder.build!( options, &block ) )
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def initialize( options = {}, api_options: nil )
|
|
57
|
+
@options = self.class.builder.build( options || {} )
|
|
58
|
+
@options = api_options.merge( @options ) if api_options
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_h
|
|
62
|
+
@options.to_h
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Exa
|
|
2
|
+
class FindSimilarRequest < Request
|
|
3
|
+
|
|
4
|
+
def submit( url, options = nil, &block )
|
|
5
|
+
if options
|
|
6
|
+
options = options.is_a?( FindSimilarOptions ) ? options : FindSimilarOptions.build!( options.to_h )
|
|
7
|
+
options = options.to_h
|
|
8
|
+
else
|
|
9
|
+
options = {}
|
|
10
|
+
end
|
|
11
|
+
options[ :url ] = url.to_s
|
|
12
|
+
|
|
13
|
+
response = post( "#{ BASE_URI }/findSimilar", options, &block )
|
|
14
|
+
attributes = ( JSON.parse( response.body, symbolize_names: true ) rescue nil )
|
|
15
|
+
|
|
16
|
+
result = if response.success?
|
|
17
|
+
FindSimilarResult.new( attributes )
|
|
18
|
+
else
|
|
19
|
+
ErrorResult.new( response.status, attributes )
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
ResponseMethods.install( response, result )
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Exa
|
|
2
|
+
|
|
3
|
+
FindSimilarResultItemSchema = DynamicSchema::Struct.define do
|
|
4
|
+
id String
|
|
5
|
+
url String
|
|
6
|
+
title String
|
|
7
|
+
score Float
|
|
8
|
+
published_date String, as: :publishedDate
|
|
9
|
+
author String
|
|
10
|
+
text String
|
|
11
|
+
highlights String, array: true
|
|
12
|
+
highlight_scores Float, array: true, as: :highlightScores
|
|
13
|
+
summary String
|
|
14
|
+
image String
|
|
15
|
+
favicon String
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class FindSimilarResultItem < FindSimilarResultItemSchema
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
FindSimilarResultSchema = DynamicSchema::Struct.define do
|
|
22
|
+
request_id String, as: :requestId
|
|
23
|
+
results FindSimilarResultItem, array: true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class FindSimilarResult < FindSimilarResultSchema
|
|
27
|
+
def success?
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
end
|
data/lib/exa/helpers.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Exa
|
|
2
|
+
module ModuleMethods
|
|
3
|
+
|
|
4
|
+
def connection( connection = nil )
|
|
5
|
+
@connection = connection if connection
|
|
6
|
+
@connection ||= Faraday.new { | builder | builder.adapter Faraday.default_adapter }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def api_key( api_key = nil )
|
|
10
|
+
@api_key = api_key || @api_key
|
|
11
|
+
@api_key
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def search( query, options = nil, &block )
|
|
15
|
+
Exa::SearchRequest.new.submit( query, options, &block )
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def contents( urls, options = nil, &block )
|
|
19
|
+
Exa::ContentsRequest.new.submit( urls, options, &block )
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def find_similar( url, options = nil, &block )
|
|
23
|
+
Exa::FindSimilarRequest.new.submit( url, options, &block )
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def answer( query, options = nil, &block )
|
|
27
|
+
Exa::AnswerRequest.new.submit( query, options, &block )
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/exa/request.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Exa
|
|
2
|
+
class Request
|
|
3
|
+
|
|
4
|
+
BASE_URI = 'https://api.exa.ai'
|
|
5
|
+
|
|
6
|
+
def initialize( connection: nil, api_key: nil )
|
|
7
|
+
@connection = connection || Exa.connection
|
|
8
|
+
@api_key = api_key || Exa.api_key
|
|
9
|
+
raise ArgumentError, "An 'api_key' is required unless configured using 'Exa.api_key'." \
|
|
10
|
+
unless @api_key
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
protected
|
|
14
|
+
|
|
15
|
+
def post( uri, body, &block )
|
|
16
|
+
headers = {
|
|
17
|
+
'x-api-key' => @api_key,
|
|
18
|
+
'Content-Type' => 'application/json'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@connection.post( uri ) do | request |
|
|
22
|
+
headers.each { | key, value | request.headers[ key ] = value }
|
|
23
|
+
request.body = body.is_a?( String ) ? body : JSON.generate( body )
|
|
24
|
+
block.call( request ) if block
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def get( uri, &block )
|
|
29
|
+
headers = {
|
|
30
|
+
'x-api-key' => @api_key,
|
|
31
|
+
'Content-Type' => 'application/json'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@connection.get( uri ) do | request |
|
|
35
|
+
headers.each { | key, value | request.headers[ key ] = value }
|
|
36
|
+
block.call( request ) if block
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module Exa
|
|
2
|
+
class SearchOptions
|
|
3
|
+
include DynamicSchema::Definable
|
|
4
|
+
include Helpers
|
|
5
|
+
|
|
6
|
+
SEARCH_TYPES = [ :neural, :keyword, :auto ]
|
|
7
|
+
CATEGORIES = [ :company, :research_paper, :news, :pdf, :github, :tweet, :personal_site,
|
|
8
|
+
:linkedin_profile, :financial_report ]
|
|
9
|
+
LIVECRAWL_OPTIONS = [ :never, :fallback, :always ]
|
|
10
|
+
|
|
11
|
+
schema do
|
|
12
|
+
|
|
13
|
+
# search configuration
|
|
14
|
+
type Symbol, in: SEARCH_TYPES
|
|
15
|
+
category Symbol, in: CATEGORIES
|
|
16
|
+
use_autoprompt [ TrueClass, FalseClass ], as: :useAutoprompt
|
|
17
|
+
user_location String, as: :userLocation
|
|
18
|
+
|
|
19
|
+
# result control
|
|
20
|
+
num_results Integer, as: :numResults
|
|
21
|
+
|
|
22
|
+
# domain filtering
|
|
23
|
+
include_domains String, array: true, as: :includeDomains
|
|
24
|
+
exclude_domains String, array: true, as: :excludeDomains
|
|
25
|
+
|
|
26
|
+
# date filtering
|
|
27
|
+
start_crawl_date String, as: :startCrawlDate
|
|
28
|
+
end_crawl_date String, as: :endCrawlDate
|
|
29
|
+
start_published_date String, as: :startPublishedDate
|
|
30
|
+
end_published_date String, as: :endPublishedDate
|
|
31
|
+
|
|
32
|
+
# content filtering
|
|
33
|
+
include_text String, array: true, as: :includeText
|
|
34
|
+
exclude_text String, array: true, as: :excludeText
|
|
35
|
+
|
|
36
|
+
# content retrieval options
|
|
37
|
+
contents do
|
|
38
|
+
text do
|
|
39
|
+
max_characters Integer, as: :maxCharacters
|
|
40
|
+
include_html_tags [ TrueClass, FalseClass ], as: :includeHtmlTags
|
|
41
|
+
end
|
|
42
|
+
highlights do
|
|
43
|
+
num_sentences Integer, as: :numSentences
|
|
44
|
+
highlights_per_url Integer, as: :highlightsPerUrl
|
|
45
|
+
query String
|
|
46
|
+
end
|
|
47
|
+
summary do
|
|
48
|
+
query String
|
|
49
|
+
end
|
|
50
|
+
livecrawl Symbol, in: LIVECRAWL_OPTIONS
|
|
51
|
+
livecrawl_timeout Integer, as: :livecrawlTimeout
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.build( options = nil, &block )
|
|
57
|
+
new( api_options: builder.build( options, &block ) )
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.build!( options = nil, &block )
|
|
61
|
+
new( api_options: builder.build!( options, &block ) )
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def initialize( options = {}, api_options: nil )
|
|
65
|
+
@options = self.class.builder.build( options || {} )
|
|
66
|
+
@options = api_options.merge( @options ) if api_options
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_h
|
|
70
|
+
@options.to_h
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Exa
|
|
2
|
+
class SearchRequest < Request
|
|
3
|
+
|
|
4
|
+
def submit( query, options = nil, &block )
|
|
5
|
+
if options
|
|
6
|
+
options = options.is_a?( SearchOptions ) ? options : SearchOptions.build!( options.to_h )
|
|
7
|
+
options = options.to_h
|
|
8
|
+
else
|
|
9
|
+
options = {}
|
|
10
|
+
end
|
|
11
|
+
options[ :query ] = query.to_s
|
|
12
|
+
|
|
13
|
+
response = post( "#{ BASE_URI }/search", options, &block )
|
|
14
|
+
attributes = ( JSON.parse( response.body, symbolize_names: true ) rescue nil )
|
|
15
|
+
|
|
16
|
+
result = if response.success?
|
|
17
|
+
SearchResult.new( attributes )
|
|
18
|
+
else
|
|
19
|
+
ErrorResult.new( response.status, attributes )
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
ResponseMethods.install( response, result )
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Exa
|
|
2
|
+
|
|
3
|
+
SearchResultItemSchema = DynamicSchema::Struct.define do
|
|
4
|
+
id String
|
|
5
|
+
url String
|
|
6
|
+
title String
|
|
7
|
+
score Float
|
|
8
|
+
published_date String, as: :publishedDate
|
|
9
|
+
author String
|
|
10
|
+
text String
|
|
11
|
+
highlights String, array: true
|
|
12
|
+
highlight_scores Float, array: true, as: :highlightScores
|
|
13
|
+
summary String
|
|
14
|
+
image String
|
|
15
|
+
favicon String
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class SearchResultItem < SearchResultItemSchema
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
AutopromptSchema = DynamicSchema::Struct.define do
|
|
22
|
+
query String
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class Autoprompt < AutopromptSchema
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
SearchResultSchema = DynamicSchema::Struct.define do
|
|
29
|
+
request_id String, as: :requestId
|
|
30
|
+
resolved_search_type String, as: :resolvedSearchType
|
|
31
|
+
autoprompt_string String, as: :autopromptString
|
|
32
|
+
autoprompt Autoprompt
|
|
33
|
+
results SearchResultItem, array: true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class SearchResult < SearchResultSchema
|
|
37
|
+
def success?
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
data/lib/exa/version.rb
ADDED
data/lib/exa.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'uri'
|
|
3
|
+
|
|
4
|
+
require 'faraday'
|
|
5
|
+
require 'dynamic_schema'
|
|
6
|
+
|
|
7
|
+
require_relative 'exa/version'
|
|
8
|
+
|
|
9
|
+
require_relative 'exa/helpers'
|
|
10
|
+
require_relative 'exa/error_result'
|
|
11
|
+
require_relative 'exa/request'
|
|
12
|
+
require_relative 'exa/response_methods'
|
|
13
|
+
|
|
14
|
+
require_relative 'exa/search_options'
|
|
15
|
+
require_relative 'exa/search_result'
|
|
16
|
+
require_relative 'exa/search_request'
|
|
17
|
+
|
|
18
|
+
require_relative 'exa/contents_options'
|
|
19
|
+
require_relative 'exa/contents_result'
|
|
20
|
+
require_relative 'exa/contents_request'
|
|
21
|
+
|
|
22
|
+
require_relative 'exa/find_similar_options'
|
|
23
|
+
require_relative 'exa/find_similar_result'
|
|
24
|
+
require_relative 'exa/find_similar_request'
|
|
25
|
+
|
|
26
|
+
require_relative 'exa/answer_options'
|
|
27
|
+
require_relative 'exa/answer_result'
|
|
28
|
+
require_relative 'exa/answer_request'
|
|
29
|
+
|
|
30
|
+
require_relative 'exa/module_methods'
|
|
31
|
+
|
|
32
|
+
module Exa
|
|
33
|
+
extend ModuleMethods
|
|
34
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: exa-rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kristoph Cichocki-Romanov
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: dynamicschema
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minitest
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.25'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '5.25'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: debug
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.9'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.9'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: vcr
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '6.3'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '6.3'
|
|
82
|
+
description: |-
|
|
83
|
+
The Exa gem implements a lightweight interface to the Exa.ai API. Exa provides powerful neural search capabilities that go beyond traditional keyword matching to understand semantic meaning and context.
|
|
84
|
+
|
|
85
|
+
This gem supports search, find similar, answer, and contents endpoints. It includes both a Ruby API and a command-line interface (exa) for quick searches from the terminal.
|
|
86
|
+
email:
|
|
87
|
+
- rubygems.org@kristoph.net
|
|
88
|
+
executables:
|
|
89
|
+
- exa
|
|
90
|
+
extensions: []
|
|
91
|
+
extra_rdoc_files: []
|
|
92
|
+
files:
|
|
93
|
+
- LICENSE
|
|
94
|
+
- README.md
|
|
95
|
+
- bin/exa
|
|
96
|
+
- exa-rb.gemspec
|
|
97
|
+
- lib/exa.rb
|
|
98
|
+
- lib/exa/answer_options.rb
|
|
99
|
+
- lib/exa/answer_request.rb
|
|
100
|
+
- lib/exa/answer_result.rb
|
|
101
|
+
- lib/exa/contents_options.rb
|
|
102
|
+
- lib/exa/contents_request.rb
|
|
103
|
+
- lib/exa/contents_result.rb
|
|
104
|
+
- lib/exa/error_result.rb
|
|
105
|
+
- lib/exa/find_similar_options.rb
|
|
106
|
+
- lib/exa/find_similar_request.rb
|
|
107
|
+
- lib/exa/find_similar_result.rb
|
|
108
|
+
- lib/exa/helpers.rb
|
|
109
|
+
- lib/exa/module_methods.rb
|
|
110
|
+
- lib/exa/request.rb
|
|
111
|
+
- lib/exa/response_methods.rb
|
|
112
|
+
- lib/exa/search_options.rb
|
|
113
|
+
- lib/exa/search_request.rb
|
|
114
|
+
- lib/exa/search_result.rb
|
|
115
|
+
- lib/exa/version.rb
|
|
116
|
+
homepage: https://github.com/EndlessInternational/exa-rb
|
|
117
|
+
licenses:
|
|
118
|
+
- MIT
|
|
119
|
+
metadata:
|
|
120
|
+
source_code_uri: https://github.com/EndlessInternational/exa-rb
|
|
121
|
+
bug_tracker_uri: https://github.com/EndlessInternational/exa-rb/issues
|
|
122
|
+
rdoc_options: []
|
|
123
|
+
require_paths:
|
|
124
|
+
- lib
|
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - ">="
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: '3.0'
|
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
131
|
+
requirements:
|
|
132
|
+
- - ">="
|
|
133
|
+
- !ruby/object:Gem::Version
|
|
134
|
+
version: '0'
|
|
135
|
+
requirements: []
|
|
136
|
+
rubygems_version: 3.6.7
|
|
137
|
+
specification_version: 4
|
|
138
|
+
summary: The Exa gem implements a lightweight interface to the Exa.ai API for neural
|
|
139
|
+
and keyword-based web search with content retrieval capabilities.
|
|
140
|
+
test_files: []
|