exaonruby 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.txt +21 -0
- data/README.md +614 -0
- data/exaonruby.gemspec +37 -0
- data/exe/exa +7 -0
- data/lib/exa/cli.rb +458 -0
- data/lib/exa/client.rb +210 -0
- data/lib/exa/configuration.rb +81 -0
- data/lib/exa/endpoints/answer.rb +109 -0
- data/lib/exa/endpoints/contents.rb +141 -0
- data/lib/exa/endpoints/events.rb +71 -0
- data/lib/exa/endpoints/find_similar.rb +154 -0
- data/lib/exa/endpoints/imports.rb +145 -0
- data/lib/exa/endpoints/monitors.rb +193 -0
- data/lib/exa/endpoints/research.rb +158 -0
- data/lib/exa/endpoints/search.rb +195 -0
- data/lib/exa/endpoints/webhooks.rb +161 -0
- data/lib/exa/endpoints/webset_enrichments.rb +162 -0
- data/lib/exa/endpoints/webset_items.rb +90 -0
- data/lib/exa/endpoints/webset_searches.rb +137 -0
- data/lib/exa/endpoints/websets.rb +214 -0
- data/lib/exa/errors.rb +180 -0
- data/lib/exa/resources/answer_response.rb +101 -0
- data/lib/exa/resources/base.rb +56 -0
- data/lib/exa/resources/contents_response.rb +123 -0
- data/lib/exa/resources/event.rb +84 -0
- data/lib/exa/resources/import.rb +137 -0
- data/lib/exa/resources/monitor.rb +205 -0
- data/lib/exa/resources/paginated_response.rb +87 -0
- data/lib/exa/resources/research_task.rb +165 -0
- data/lib/exa/resources/search_response.rb +111 -0
- data/lib/exa/resources/search_result.rb +95 -0
- data/lib/exa/resources/webhook.rb +152 -0
- data/lib/exa/resources/webset.rb +491 -0
- data/lib/exa/resources/webset_item.rb +256 -0
- data/lib/exa/utils/parameter_converter.rb +159 -0
- data/lib/exa/utils/webhook_handler.rb +239 -0
- data/lib/exa/version.rb +7 -0
- data/lib/exa.rb +130 -0
- metadata +146 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2da95920bde51eb7724ef7c1cfa01d25906d4cb04cde2d2dc9d1055ec1267f9e
|
|
4
|
+
data.tar.gz: 190c39f5437c045f60067856033ad54330ce05425dd0edea79cb0fa74fed989f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: dea6072b3c1b149c869a4d76854cd36a3aa1a18241cbdea6a105f1331e2fb297723def00c59a30e13c4c044d93095b03b1bd7594f9dc9815e8c20b3bc59c3546
|
|
7
|
+
data.tar.gz: 12db1ecb3e8b3528bcaf74c03ea9a259136d794a9c68125562d69fe1d031808582f140cb2e50eb06b2faf3b9ec476c2eefc56809fe013ad3439d63fba35a7776
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Exa Labs
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
# Exa Ruby
|
|
2
|
+
|
|
3
|
+
A production-ready Ruby gem wrapper for the [Exa.ai](https://exa.ai) API, providing intelligent web search, content extraction, and structured data collection capabilities.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Search API**: Neural search, deep search, and content extraction
|
|
8
|
+
- **Contents API**: Fetch full page contents with livecrawl support
|
|
9
|
+
- **Find Similar**: Discover semantically similar pages
|
|
10
|
+
- **Answer API**: LLM-powered question answering with citations
|
|
11
|
+
- **Research API**: Async research tasks with structured output
|
|
12
|
+
- **Websets API**: Build and manage structured web data collections
|
|
13
|
+
- **Monitors**: Automated scheduled searches and content refresh
|
|
14
|
+
- **Imports**: Upload CSV data into Websets
|
|
15
|
+
- **Webhooks & Events**: Real-time notifications for Websets activity
|
|
16
|
+
- **Beautiful CLI**: Colorful command-line interface
|
|
17
|
+
- **n8n/Zapier Integration**: Webhook signature verification utilities
|
|
18
|
+
- **Automatic Retries**: Built-in retry logic for transient failures
|
|
19
|
+
- **Rate Limit Handling**: Proper error handling with retry information
|
|
20
|
+
- **Type Documentation**: Comprehensive YARD documentation
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
Add this line to your application's Gemfile:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
gem 'exa'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
And then execute:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bundle install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or install it yourself as:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
gem install exa
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
### Configuration
|
|
45
|
+
|
|
46
|
+
Configure the client with your API key:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
require 'exa'
|
|
50
|
+
|
|
51
|
+
# Option 1: Environment variable (recommended)
|
|
52
|
+
# Set EXA_API_KEY environment variable
|
|
53
|
+
|
|
54
|
+
# Option 2: Global configuration
|
|
55
|
+
Exa.configure do |config|
|
|
56
|
+
config.api_key = 'your-api-key'
|
|
57
|
+
config.timeout = 60 # Request timeout in seconds
|
|
58
|
+
config.max_retries = 3 # Retry attempts for transient failures
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Option 3: Direct client initialization
|
|
62
|
+
client = Exa::Client.new(api_key: 'your-api-key')
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Basic Search
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
# Simple search
|
|
69
|
+
results = Exa.search("Latest developments in LLMs")
|
|
70
|
+
|
|
71
|
+
# Access results
|
|
72
|
+
results.each do |result|
|
|
73
|
+
puts "#{result.title}: #{result.url}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Search with content extraction
|
|
77
|
+
results = Exa.search(
|
|
78
|
+
"AI research papers",
|
|
79
|
+
text: true,
|
|
80
|
+
num_results: 20
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
results.each do |result|
|
|
84
|
+
puts result.title
|
|
85
|
+
puts result.text[0..500] if result.text
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Deep Search
|
|
90
|
+
|
|
91
|
+
For comprehensive results with query expansion:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
results = client.search(
|
|
95
|
+
"Machine learning startups",
|
|
96
|
+
type: :deep,
|
|
97
|
+
additional_queries: ["AI companies", "ML ventures"],
|
|
98
|
+
category: :company,
|
|
99
|
+
include_domains: ["linkedin.com", "crunchbase.com"],
|
|
100
|
+
start_published_date: "2024-01-01T00:00:00.000Z",
|
|
101
|
+
num_results: 50
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Get Contents
|
|
106
|
+
|
|
107
|
+
Fetch full page contents from URLs:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
contents = client.get_contents(
|
|
111
|
+
["https://arxiv.org/abs/2307.06435"],
|
|
112
|
+
text: true,
|
|
113
|
+
summary: true,
|
|
114
|
+
livecrawl: :preferred
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
contents.results.each do |page|
|
|
118
|
+
puts page.title
|
|
119
|
+
puts page.summary
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Check for failures
|
|
123
|
+
if !contents.all_success?
|
|
124
|
+
contents.failed_statuses.each do |status|
|
|
125
|
+
puts "Failed to fetch #{status.id}: #{status.error_tag}"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Find Similar Links
|
|
131
|
+
|
|
132
|
+
Discover pages similar to a given URL:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
similar = client.find_similar(
|
|
136
|
+
"https://arxiv.org/abs/2307.06435",
|
|
137
|
+
num_results: 20,
|
|
138
|
+
include_domains: ["arxiv.org", "paperswithcode.com"],
|
|
139
|
+
text: true
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
similar.each do |result|
|
|
143
|
+
puts "#{result.title}: #{result.url}"
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Answer API
|
|
148
|
+
|
|
149
|
+
Get LLM-powered answers to questions with citations from web sources:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
# Simple question
|
|
153
|
+
response = client.answer("What is the latest valuation of SpaceX?")
|
|
154
|
+
puts response.answer # => "$350 billion."
|
|
155
|
+
|
|
156
|
+
# Access citations
|
|
157
|
+
response.citations.each do |citation|
|
|
158
|
+
puts "Source: #{citation.title}"
|
|
159
|
+
puts "URL: #{citation.url}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# With search options
|
|
163
|
+
response = client.answer(
|
|
164
|
+
"What are the latest AI safety developments?",
|
|
165
|
+
text: true,
|
|
166
|
+
num_results: 10,
|
|
167
|
+
start_published_date: "2024-01-01T00:00:00.000Z"
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Research API
|
|
172
|
+
|
|
173
|
+
Create async research tasks for in-depth web research:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
# Create a research task
|
|
177
|
+
task = client.create_research(
|
|
178
|
+
instructions: "Summarize the latest developments in AI safety research",
|
|
179
|
+
model: "exa-research" # or "exa-research-fast", "exa-research-pro"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
puts "Task ID: #{task.research_id}"
|
|
183
|
+
puts "Status: #{task.status}" # => "pending" or "running"
|
|
184
|
+
|
|
185
|
+
# Poll for results
|
|
186
|
+
loop do
|
|
187
|
+
task = client.get_research(task.research_id)
|
|
188
|
+
|
|
189
|
+
case task.status
|
|
190
|
+
when "completed"
|
|
191
|
+
puts task.output
|
|
192
|
+
break
|
|
193
|
+
when "running"
|
|
194
|
+
puts "Progress: #{task.operations_completed}/#{task.operations_total}"
|
|
195
|
+
sleep 5
|
|
196
|
+
when "failed"
|
|
197
|
+
puts "Error: #{task.error_message}"
|
|
198
|
+
break
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# With structured output schema
|
|
203
|
+
schema = {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
companies: { type: "array", items: { type: "string" } },
|
|
207
|
+
summary: { type: "string" }
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
task = client.create_research(
|
|
212
|
+
instructions: "Find the top 5 AI startups in 2024",
|
|
213
|
+
model: "exa-research-pro",
|
|
214
|
+
output_schema: schema
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# List all research tasks
|
|
218
|
+
response = client.list_research(limit: 10)
|
|
219
|
+
response.data.each { |t| puts "#{t.research_id}: #{t.status}" }
|
|
220
|
+
|
|
221
|
+
# Cancel a running task
|
|
222
|
+
client.cancel_research(task.research_id)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Websets API
|
|
226
|
+
|
|
227
|
+
Websets allow you to build structured collections of web data with automated search, verification, and enrichment.
|
|
228
|
+
|
|
229
|
+
### Create a Webset
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
webset = client.create_webset(
|
|
233
|
+
search: {
|
|
234
|
+
query: "AI startups founded in 2024",
|
|
235
|
+
count: 100,
|
|
236
|
+
entity: { type: "company" },
|
|
237
|
+
criteria: [
|
|
238
|
+
{ description: "Company must be focused on artificial intelligence" },
|
|
239
|
+
{ description: "Founded in 2024" }
|
|
240
|
+
]
|
|
241
|
+
},
|
|
242
|
+
enrichments: [
|
|
243
|
+
{ description: "Company's total funding amount", format: "number" },
|
|
244
|
+
{ description: "Number of employees", format: "number" },
|
|
245
|
+
{
|
|
246
|
+
description: "Primary industry vertical",
|
|
247
|
+
format: "enum",
|
|
248
|
+
options: [
|
|
249
|
+
{ label: "Healthcare" },
|
|
250
|
+
{ label: "Finance" },
|
|
251
|
+
{ label: "Enterprise" },
|
|
252
|
+
{ label: "Consumer" },
|
|
253
|
+
{ label: "Other" }
|
|
254
|
+
]
|
|
255
|
+
}
|
|
256
|
+
],
|
|
257
|
+
external_id: "my-ai-startups-2024"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
puts "Created Webset: #{webset.id}"
|
|
261
|
+
puts "Status: #{webset.status}"
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Monitor Webset Progress
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
webset = client.get_webset(webset.id)
|
|
268
|
+
|
|
269
|
+
webset.searches.each do |search|
|
|
270
|
+
puts "Search: #{search.query}"
|
|
271
|
+
puts "Found: #{search.found_count}"
|
|
272
|
+
puts "Completion: #{search.completion_percentage}%"
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### List Webset Items
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
response = client.list_webset_items(webset.id, limit: 50)
|
|
280
|
+
|
|
281
|
+
response.data.each do |item|
|
|
282
|
+
puts "Item: #{item.url}"
|
|
283
|
+
puts "Type: #{item.type}"
|
|
284
|
+
|
|
285
|
+
# Check criteria evaluations
|
|
286
|
+
item.evaluations.each do |eval|
|
|
287
|
+
status = eval.satisfied? ? "✓" : "✗"
|
|
288
|
+
puts " #{status} #{eval.criterion}"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Access enrichment results
|
|
292
|
+
item.enrichments.each do |enrichment|
|
|
293
|
+
puts " #{enrichment.format}: #{enrichment.result}"
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Paginate through all items
|
|
298
|
+
while response.has_more?
|
|
299
|
+
response = client.list_webset_items(webset.id, cursor: response.next_cursor)
|
|
300
|
+
# Process items...
|
|
301
|
+
end
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Add Searches to Existing Webset
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
search = client.create_webset_search(
|
|
308
|
+
webset.id,
|
|
309
|
+
query: "AI healthcare startups",
|
|
310
|
+
count: 50,
|
|
311
|
+
entity: { type: "company" },
|
|
312
|
+
criteria: [{ description: "Must be in healthcare industry" }]
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
puts "Search ID: #{search.id}"
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Add Enrichments
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
enrichment = client.create_webset_enrichment(
|
|
322
|
+
webset.id,
|
|
323
|
+
description: "CEO or founder name",
|
|
324
|
+
format: "text"
|
|
325
|
+
)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Delete Resources
|
|
329
|
+
|
|
330
|
+
```ruby
|
|
331
|
+
# Delete an item
|
|
332
|
+
client.delete_webset_item(webset.id, item.id)
|
|
333
|
+
|
|
334
|
+
# Delete an enrichment
|
|
335
|
+
client.delete_webset_enrichment(webset.id, enrichment.id)
|
|
336
|
+
|
|
337
|
+
# Cancel an in-progress search
|
|
338
|
+
client.cancel_webset_search(webset.id, search.id)
|
|
339
|
+
|
|
340
|
+
# Delete entire webset
|
|
341
|
+
client.delete_webset(webset.id)
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## Search Options Reference
|
|
345
|
+
|
|
346
|
+
| Option | Type | Description |
|
|
347
|
+
|--------|------|-------------|
|
|
348
|
+
| `type` | Symbol | `:neural`, `:auto`, `:fast`, `:deep` |
|
|
349
|
+
| `category` | Symbol | `:people`, `:company`, `:research_paper`, `:news`, `:pdf`, `:github`, `:tweet`, `:personal_site`, `:financial_report` |
|
|
350
|
+
| `num_results` | Integer | Number of results (max 100) |
|
|
351
|
+
| `include_domains` | Array | Domains to include |
|
|
352
|
+
| `exclude_domains` | Array | Domains to exclude |
|
|
353
|
+
| `start_crawl_date` | String/Time | Results crawled after this date |
|
|
354
|
+
| `end_crawl_date` | String/Time | Results crawled before this date |
|
|
355
|
+
| `start_published_date` | String/Time | Results published after this date |
|
|
356
|
+
| `end_published_date` | String/Time | Results published before this date |
|
|
357
|
+
| `include_text` | Array | Keywords that must be present |
|
|
358
|
+
| `exclude_text` | Array | Keywords to exclude |
|
|
359
|
+
| `country` | String | Two-letter ISO country code |
|
|
360
|
+
| `text` | Boolean/Hash | Return text content |
|
|
361
|
+
| `highlights` | Boolean/Hash | Return highlights |
|
|
362
|
+
| `summary` | Boolean/Hash | Return AI summary |
|
|
363
|
+
| `context` | Boolean/Integer | Return context string for LLM |
|
|
364
|
+
| `moderation` | Boolean | Filter unsafe content |
|
|
365
|
+
| `livecrawl` | Symbol | `:never`, `:fallback`, `:preferred`, `:always` |
|
|
366
|
+
|
|
367
|
+
## Error Handling
|
|
368
|
+
|
|
369
|
+
The gem provides specific error classes for different failure modes:
|
|
370
|
+
|
|
371
|
+
```ruby
|
|
372
|
+
begin
|
|
373
|
+
results = client.search("query")
|
|
374
|
+
rescue Exa::AuthenticationError => e
|
|
375
|
+
puts "Invalid API key: #{e.message}"
|
|
376
|
+
rescue Exa::RateLimitError => e
|
|
377
|
+
puts "Rate limited. Retry after: #{e.retry_after} seconds"
|
|
378
|
+
rescue Exa::InvalidRequestError => e
|
|
379
|
+
puts "Invalid request: #{e.message}"
|
|
380
|
+
puts "Validation errors: #{e.validation_errors}" if e.validation_errors
|
|
381
|
+
rescue Exa::NotFoundError => e
|
|
382
|
+
puts "Resource not found: #{e.message}"
|
|
383
|
+
rescue Exa::ServerError => e
|
|
384
|
+
puts "Server error: #{e.message}"
|
|
385
|
+
rescue Exa::TimeoutError => e
|
|
386
|
+
puts "Request timed out: #{e.message}"
|
|
387
|
+
rescue Exa::Error => e
|
|
388
|
+
puts "General error: #{e.message}"
|
|
389
|
+
end
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Advanced Configuration
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
Exa.configure do |config|
|
|
396
|
+
config.api_key = ENV['EXA_API_KEY']
|
|
397
|
+
config.base_url = 'https://api.exa.ai'
|
|
398
|
+
config.websets_base_url = 'https://api.exa.ai/websets/v0'
|
|
399
|
+
config.timeout = 120
|
|
400
|
+
config.max_retries = 5
|
|
401
|
+
config.retry_delay = 1.0
|
|
402
|
+
config.max_retry_delay = 60.0
|
|
403
|
+
config.retry_statuses = [429, 500, 502, 503, 504]
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Enable logging
|
|
407
|
+
Exa.logger = Logger.new(STDOUT)
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## Response Objects
|
|
411
|
+
|
|
412
|
+
### SearchResponse
|
|
413
|
+
|
|
414
|
+
```ruby
|
|
415
|
+
response = client.search("query")
|
|
416
|
+
|
|
417
|
+
response.request_id # Unique request ID
|
|
418
|
+
response.results # Array of SearchResult
|
|
419
|
+
response.context # Combined context string
|
|
420
|
+
response.cost # CostInfo object
|
|
421
|
+
response.total_cost # Cost in dollars
|
|
422
|
+
|
|
423
|
+
# Enumerable
|
|
424
|
+
response.each { |r| puts r.title }
|
|
425
|
+
response.first
|
|
426
|
+
response.count
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### SearchResult
|
|
430
|
+
|
|
431
|
+
```ruby
|
|
432
|
+
result.id # Unique identifier
|
|
433
|
+
result.title # Page title
|
|
434
|
+
result.url # Page URL
|
|
435
|
+
result.published_date # Publication date (Time)
|
|
436
|
+
result.author # Author name
|
|
437
|
+
result.text # Full text content
|
|
438
|
+
result.highlights # Highlighted snippets
|
|
439
|
+
result.summary # AI-generated summary
|
|
440
|
+
result.subpages # Related subpages
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Webset
|
|
444
|
+
|
|
445
|
+
```ruby
|
|
446
|
+
webset.id # Unique identifier
|
|
447
|
+
webset.status # idle, pending, running, paused
|
|
448
|
+
webset.title # Webset title
|
|
449
|
+
webset.searches # Array of WebsetSearch
|
|
450
|
+
webset.items # Array of WebsetItem
|
|
451
|
+
webset.enrichments # Array of WebsetEnrichment
|
|
452
|
+
webset.created_at # Creation timestamp
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## Monitors
|
|
456
|
+
|
|
457
|
+
Create automated schedules to keep Websets updated:
|
|
458
|
+
|
|
459
|
+
```ruby
|
|
460
|
+
# Create a monitor to search daily
|
|
461
|
+
monitor = client.create_monitor(
|
|
462
|
+
webset_id: "webset_abc123",
|
|
463
|
+
cadence: { cron: "0 9 * * *", timezone: "America/New_York" },
|
|
464
|
+
behavior: {
|
|
465
|
+
type: "search",
|
|
466
|
+
config: {
|
|
467
|
+
count: 50,
|
|
468
|
+
query: "AI news today",
|
|
469
|
+
entity: { type: "article" },
|
|
470
|
+
behavior: "append"
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# List monitors
|
|
476
|
+
client.list_monitors(webset_id: "webset_abc123")
|
|
477
|
+
|
|
478
|
+
# Update monitor
|
|
479
|
+
client.update_monitor(monitor.id, status: "disabled")
|
|
480
|
+
|
|
481
|
+
# Delete monitor
|
|
482
|
+
client.delete_monitor(monitor.id)
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
## Imports
|
|
486
|
+
|
|
487
|
+
Upload CSV data into Websets:
|
|
488
|
+
|
|
489
|
+
```ruby
|
|
490
|
+
# Create an import
|
|
491
|
+
import = client.create_import(
|
|
492
|
+
size: 1024,
|
|
493
|
+
count: 100,
|
|
494
|
+
format: "csv",
|
|
495
|
+
entity: { type: "company" },
|
|
496
|
+
title: "Q4 Leads",
|
|
497
|
+
csv: { identifier: 1 }
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
# Upload file to the returned URL
|
|
501
|
+
puts "Upload to: #{import.upload_url}"
|
|
502
|
+
puts "Valid until: #{import.upload_valid_until}"
|
|
503
|
+
|
|
504
|
+
# Check import status
|
|
505
|
+
import = client.get_import(import.id)
|
|
506
|
+
puts "Status: #{import.status}"
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
## Webhooks & Events
|
|
510
|
+
|
|
511
|
+
Subscribe to real-time notifications:
|
|
512
|
+
|
|
513
|
+
```ruby
|
|
514
|
+
# Create a webhook
|
|
515
|
+
webhook = client.create_webhook(
|
|
516
|
+
url: "https://example.com/webhooks/exa",
|
|
517
|
+
events: ["webset.item.created", "webset.item.enriched"]
|
|
518
|
+
)
|
|
519
|
+
puts "Secret (save this!): #{webhook.secret}"
|
|
520
|
+
|
|
521
|
+
# List events
|
|
522
|
+
events = client.list_events(
|
|
523
|
+
types: ["webset.item.created"],
|
|
524
|
+
limit: 50
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
events.data.each { |e| puts "#{e.type} at #{e.created_at}" }
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
## n8n & Zapier Integration
|
|
531
|
+
|
|
532
|
+
Verify webhook signatures in your integrations:
|
|
533
|
+
|
|
534
|
+
```ruby
|
|
535
|
+
# In your webhook receiver (Rails, Sinatra, etc.)
|
|
536
|
+
raw_body = request.raw_post
|
|
537
|
+
signature = request.headers["X-Exa-Signature"]
|
|
538
|
+
|
|
539
|
+
if Exa::Utils::WebhookHandler.verify_signature(
|
|
540
|
+
raw_body,
|
|
541
|
+
signature,
|
|
542
|
+
secret: ENV["EXA_WEBHOOK_SECRET"]
|
|
543
|
+
)
|
|
544
|
+
event = Exa::Utils::WebhookHandler.parse_event(raw_body)
|
|
545
|
+
|
|
546
|
+
case event.type
|
|
547
|
+
when "webset.item.created"
|
|
548
|
+
# Handle new item
|
|
549
|
+
when "webset.item.enriched"
|
|
550
|
+
# Handle enriched item
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
head :ok
|
|
554
|
+
else
|
|
555
|
+
head :unauthorized
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# One-liner with automatic error handling
|
|
559
|
+
event = Exa::Utils::WebhookHandler.construct_event(
|
|
560
|
+
raw_body,
|
|
561
|
+
request.headers,
|
|
562
|
+
secret: ENV["EXA_WEBHOOK_SECRET"]
|
|
563
|
+
)
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
## Command-Line Interface
|
|
567
|
+
|
|
568
|
+
The gem includes a beautiful CLI:
|
|
569
|
+
|
|
570
|
+
```bash
|
|
571
|
+
# Search
|
|
572
|
+
exa search "latest AI research"
|
|
573
|
+
exa search "ML startups" -n 20 --type deep
|
|
574
|
+
|
|
575
|
+
# Answer questions
|
|
576
|
+
exa answer "What is the valuation of SpaceX?"
|
|
577
|
+
|
|
578
|
+
# Find similar pages
|
|
579
|
+
exa similar "https://arxiv.org/abs/2307.06435"
|
|
580
|
+
|
|
581
|
+
# Research
|
|
582
|
+
exa research "Summarize AI safety developments in 2024" --wait
|
|
583
|
+
|
|
584
|
+
# Manage Websets
|
|
585
|
+
exa websets list
|
|
586
|
+
exa websets get ws_abc123
|
|
587
|
+
exa websets items ws_abc123
|
|
588
|
+
|
|
589
|
+
# Output as JSON
|
|
590
|
+
exa search "AI news" --json
|
|
591
|
+
|
|
592
|
+
# Show version
|
|
593
|
+
exa version
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
## Requirements
|
|
597
|
+
|
|
598
|
+
- Ruby >= 3.1
|
|
599
|
+
- faraday >= 2.0
|
|
600
|
+
- faraday-retry >= 2.0
|
|
601
|
+
- thor >= 1.0
|
|
602
|
+
|
|
603
|
+
## Development
|
|
604
|
+
|
|
605
|
+
After checking out the repo, run `bundle install` to install dependencies.
|
|
606
|
+
|
|
607
|
+
## License
|
|
608
|
+
|
|
609
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
610
|
+
|
|
611
|
+
## Contributing
|
|
612
|
+
|
|
613
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/exa-labs/exa-ruby.
|
|
614
|
+
|
data/exaonruby.gemspec
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/exa/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "exaonruby"
|
|
7
|
+
spec.version = Exa::VERSION
|
|
8
|
+
spec.authors = ["tigel-agm"]
|
|
9
|
+
spec.email = []
|
|
10
|
+
|
|
11
|
+
spec.summary = "Complete Ruby client for the Exa.ai API with beautiful CLI"
|
|
12
|
+
spec.description = "A production-ready Ruby gem wrapper for the Exa.ai Search and Websets APIs. " \
|
|
13
|
+
"Features neural search, LLM-powered answers, async research tasks, " \
|
|
14
|
+
"Websets management (monitors, imports, webhooks), and a beautiful CLI. " \
|
|
15
|
+
"Includes n8n/Zapier webhook signature verification utilities."
|
|
16
|
+
spec.homepage = "https://github.com/tigel-agm/exaonruby"
|
|
17
|
+
spec.license = "MIT"
|
|
18
|
+
spec.required_ruby_version = ">= 3.1"
|
|
19
|
+
|
|
20
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
21
|
+
spec.metadata["source_code_uri"] = "https://github.com/tigel-agm/exaonruby"
|
|
22
|
+
spec.metadata["changelog_uri"] = "https://github.com/tigel-agm/exaonruby/blob/main/CHANGELOG.md"
|
|
23
|
+
spec.metadata["documentation_uri"] = "https://github.com/tigel-agm/exaonruby#readme"
|
|
24
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
25
|
+
|
|
26
|
+
# Include all lib files explicitly since we may not have git
|
|
27
|
+
spec.files = Dir.glob("{lib,exe}/**/*") + %w[LICENSE.txt README.md]
|
|
28
|
+
spec.files += Dir.glob("*.gemspec")
|
|
29
|
+
|
|
30
|
+
spec.bindir = "exe"
|
|
31
|
+
spec.executables = ["exa"]
|
|
32
|
+
spec.require_paths = ["lib"]
|
|
33
|
+
|
|
34
|
+
spec.add_dependency "faraday", ">= 2.0", "< 3.0"
|
|
35
|
+
spec.add_dependency "faraday-retry", ">= 2.0", "< 3.0"
|
|
36
|
+
spec.add_dependency "thor", ">= 1.0", "< 3.0"
|
|
37
|
+
end
|