exa-ai 0.1.0 → 0.3.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/README.md +137 -4
- data/exe/exa-ai-answer +31 -7
- data/exe/exa-ai-get-contents +190 -34
- data/exe/exa-ai-research-start +2 -2
- data/exe/exa-ai-search +191 -17
- data/lib/exa/client.rb +41 -0
- data/lib/exa/services/answer_stream.rb +90 -0
- data/lib/exa/services/parameter_converter.rb +122 -0
- data/lib/exa/services/search.rb +3 -1
- data/lib/exa/version.rb +1 -1
- data/lib/exa.rb +1 -0
- metadata +17 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ae8ad5762410325124acdcada89aed1a88b18dcbecc8b85431b99b0c05e0f354
|
4
|
+
data.tar.gz: b1d8ec27baf9bdf324af7183a6088e22e2437ef5d0bb0c97edee47eadeeed7de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1803b1811bc368be4d83bd9b02aa095c4f77f0dd97e05f3bd96ab9bc380d54b9a9bb7e17f5e3fca78c7a2f7348000f61bc55ec4875273eafa6dd0762fb09f7aa
|
7
|
+
data.tar.gz: 0ec026a9d5b4d05c8779aa3e10df8c25c48e5a728f92608941e28f2b18a46238908af53837dfa234b22f2ad595922f03d1ce25f98f6b9f932ecce8570914a8aa
|
data/README.md
CHANGED
@@ -214,16 +214,150 @@ exa-ai search "tutorials" \
|
|
214
214
|
exa-ai search "AI" --output-format pretty
|
215
215
|
```
|
216
216
|
|
217
|
-
**Options:**
|
217
|
+
**Basic Options:**
|
218
218
|
- `QUERY` - Search query (required)
|
219
219
|
- `--num-results N` - Number of results (default: 10)
|
220
220
|
- `--type TYPE` - Search type: keyword, neural, or auto (default: auto)
|
221
221
|
- `--include-domains DOMAINS` - Comma-separated domains to include
|
222
222
|
- `--exclude-domains DOMAINS` - Comma-separated domains to exclude
|
223
|
-
- `--use-autoprompt` - Use Exa's autoprompt feature
|
224
223
|
- `--output-format FORMAT` - json or pretty (default: json)
|
225
224
|
- `--api-key KEY` - API key (or set EXA_API_KEY env var)
|
226
225
|
|
226
|
+
#### Advanced Search Options
|
227
|
+
|
228
|
+
**Date Filtering:**
|
229
|
+
```bash
|
230
|
+
# Filter by published date
|
231
|
+
exa-ai search "AI research" \
|
232
|
+
--start-published-date "2025-01-01T00:00:00.000Z" \
|
233
|
+
--end-published-date "2025-12-31T23:59:59.999Z"
|
234
|
+
|
235
|
+
# Filter by crawl date
|
236
|
+
exa-ai search "news" \
|
237
|
+
--start-crawl-date "2025-10-01T00:00:00.000Z" \
|
238
|
+
--end-crawl-date "2025-10-31T23:59:59.999Z"
|
239
|
+
```
|
240
|
+
|
241
|
+
**Text Filtering:**
|
242
|
+
```bash
|
243
|
+
# Results must include specific phrase
|
244
|
+
exa-ai search "machine learning" --include-text "neural networks"
|
245
|
+
|
246
|
+
# Results must exclude specific phrase
|
247
|
+
exa-ai search "programming" --exclude-text "paid-partnership"
|
248
|
+
|
249
|
+
# Combine inclusion and exclusion
|
250
|
+
exa-ai search "Python" \
|
251
|
+
--include-text "open source" \
|
252
|
+
--exclude-text "deprecated"
|
253
|
+
```
|
254
|
+
|
255
|
+
**Content Extraction:**
|
256
|
+
```bash
|
257
|
+
# Extract full webpage text
|
258
|
+
exa-ai search "Ruby" --text
|
259
|
+
|
260
|
+
# Extract text with options
|
261
|
+
exa-ai search "AI" \
|
262
|
+
--text \
|
263
|
+
--text-max-characters 3000 \
|
264
|
+
--include-html-tags
|
265
|
+
|
266
|
+
# Generate AI summaries
|
267
|
+
exa-ai search "climate change" \
|
268
|
+
--summary \
|
269
|
+
--summary-query "What are the main points?"
|
270
|
+
|
271
|
+
# Format results as context for LLM RAG
|
272
|
+
exa-ai search "kubernetes" \
|
273
|
+
--context \
|
274
|
+
--context-max-characters 5000
|
275
|
+
|
276
|
+
# Crawl subpages
|
277
|
+
exa-ai search "documentation" \
|
278
|
+
--subpages 1 \
|
279
|
+
--subpage-target about \
|
280
|
+
--subpage-target docs
|
281
|
+
|
282
|
+
# Extract links from results
|
283
|
+
exa-ai search "web development" \
|
284
|
+
--links 3 \
|
285
|
+
--image-links 2
|
286
|
+
```
|
287
|
+
|
288
|
+
**Advanced Ruby API:**
|
289
|
+
```ruby
|
290
|
+
client = Exa::Client.new(api_key: "your-key")
|
291
|
+
|
292
|
+
# Date range filtering
|
293
|
+
results = client.search("AI research",
|
294
|
+
start_published_date: "2025-01-01T00:00:00.000Z",
|
295
|
+
end_published_date: "2025-12-31T23:59:59.999Z"
|
296
|
+
)
|
297
|
+
|
298
|
+
# Text filtering
|
299
|
+
results = client.search("machine learning",
|
300
|
+
include_text: ["neural networks"],
|
301
|
+
exclude_text: ["cryptocurrency"]
|
302
|
+
)
|
303
|
+
|
304
|
+
# Full webpage text extraction
|
305
|
+
results = client.search("Ruby",
|
306
|
+
text: {
|
307
|
+
max_characters: 3000,
|
308
|
+
include_html_tags: true
|
309
|
+
}
|
310
|
+
)
|
311
|
+
|
312
|
+
# AI-powered summaries
|
313
|
+
results = client.search("climate change",
|
314
|
+
summary: {
|
315
|
+
query: "What are the main points?"
|
316
|
+
}
|
317
|
+
)
|
318
|
+
|
319
|
+
# Context for RAG pipelines
|
320
|
+
results = client.search("kubernetes",
|
321
|
+
context: {
|
322
|
+
max_characters: 5000
|
323
|
+
}
|
324
|
+
)
|
325
|
+
|
326
|
+
# Subpage crawling
|
327
|
+
results = client.search("documentation",
|
328
|
+
subpages: 1,
|
329
|
+
subpage_target: ["about", "docs", "guide"]
|
330
|
+
)
|
331
|
+
|
332
|
+
# Links and image extraction
|
333
|
+
results = client.search("web development",
|
334
|
+
extras: {
|
335
|
+
links: 3,
|
336
|
+
image_links: 2
|
337
|
+
}
|
338
|
+
)
|
339
|
+
|
340
|
+
# Combine multiple features
|
341
|
+
results = client.search("AI",
|
342
|
+
num_results: 5,
|
343
|
+
start_published_date: "2025-01-01T00:00:00.000Z",
|
344
|
+
text: { max_characters: 3000 },
|
345
|
+
summary: { query: "Main developments?" },
|
346
|
+
context: { max_characters: 5000 },
|
347
|
+
subpages: 1,
|
348
|
+
subpage_target: ["research"],
|
349
|
+
extras: { links: 3, image_links: 2 }
|
350
|
+
)
|
351
|
+
|
352
|
+
# Access extracted content
|
353
|
+
results.results.each do |result|
|
354
|
+
puts result["title"]
|
355
|
+
puts result["text"] if result["text"] # Full webpage text
|
356
|
+
puts result["summary"] if result["summary"] # AI summary
|
357
|
+
puts result["links"] if result["links"] # Extracted links
|
358
|
+
end
|
359
|
+
```
|
360
|
+
|
227
361
|
### Answer Command
|
228
362
|
|
229
363
|
Generate comprehensive answers to questions using Exa's answer generation feature:
|
@@ -484,8 +618,7 @@ client = Exa::Client.new
|
|
484
618
|
results = client.search("kubernetes tutorial",
|
485
619
|
num_results: 20,
|
486
620
|
type: "neural",
|
487
|
-
include_domains: ["kubernetes.io", "github.com"]
|
488
|
-
use_autoprompt: true
|
621
|
+
include_domains: ["kubernetes.io", "github.com"]
|
489
622
|
)
|
490
623
|
|
491
624
|
results.results.each do |item|
|
data/exe/exa-ai-answer
CHANGED
@@ -12,7 +12,8 @@ def parse_args(argv)
|
|
12
12
|
args = {
|
13
13
|
output_format: "json",
|
14
14
|
api_key: nil,
|
15
|
-
text: false
|
15
|
+
text: false,
|
16
|
+
stream: false
|
16
17
|
}
|
17
18
|
|
18
19
|
# Extract query (first non-flag argument)
|
@@ -24,9 +25,15 @@ def parse_args(argv)
|
|
24
25
|
when "--text"
|
25
26
|
args[:text] = true
|
26
27
|
i += 1
|
28
|
+
when "--stream"
|
29
|
+
args[:stream] = true
|
30
|
+
i += 1
|
27
31
|
when "--output-schema"
|
28
32
|
args[:output_schema] = argv[i + 1]
|
29
33
|
i += 2
|
34
|
+
when "--system-prompt"
|
35
|
+
args[:system_prompt] = argv[i + 1]
|
36
|
+
i += 2
|
30
37
|
when "--api-key"
|
31
38
|
args[:api_key] = argv[i + 1]
|
32
39
|
i += 2
|
@@ -43,17 +50,21 @@ def parse_args(argv)
|
|
43
50
|
QUERY Question or query to answer (required)
|
44
51
|
|
45
52
|
Options:
|
53
|
+
--stream Stream answer chunks in real-time
|
46
54
|
--text Include full text content from sources
|
47
55
|
--output-schema JSON JSON schema for structured output
|
56
|
+
--system-prompt TEXT System prompt to guide answer generation
|
48
57
|
--api-key KEY Exa API key (or set EXA_API_KEY env var)
|
49
58
|
--output-format FMT Output format: json, pretty, or text (default: json)
|
50
59
|
--help, -h Show this help message
|
51
60
|
|
52
61
|
Examples:
|
53
62
|
exa-api answer "What is the capital of France?"
|
63
|
+
exa-api answer "Latest AI breakthroughs" --stream
|
54
64
|
exa-api answer "Latest AI breakthroughs" --text
|
55
65
|
exa-api answer "Ruby best practices" --output-format pretty
|
56
66
|
exa-api answer "What is the capital of France?" --output-schema '{"type":"object","properties":{"city":{"type":"string"},"state":{"type":"string"}}}'
|
67
|
+
exa-api answer "What is Paris?" --system-prompt "Respond in the voice of a pirate"
|
57
68
|
HELP
|
58
69
|
exit 0
|
59
70
|
else
|
@@ -89,6 +100,7 @@ begin
|
|
89
100
|
# Prepare answer parameters
|
90
101
|
answer_params = {}
|
91
102
|
answer_params[:text] = args[:text] if args[:text]
|
103
|
+
answer_params[:system_prompt] = args[:system_prompt] if args[:system_prompt]
|
92
104
|
|
93
105
|
# Parse output_schema as JSON if provided
|
94
106
|
if args[:output_schema]
|
@@ -100,12 +112,24 @@ begin
|
|
100
112
|
end
|
101
113
|
end
|
102
114
|
|
103
|
-
# Execute answer
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
115
|
+
# Execute answer or streaming answer
|
116
|
+
if args[:stream]
|
117
|
+
# Streaming mode - output chunks in real-time
|
118
|
+
client.answer_stream(args[:query], **answer_params) do |chunk|
|
119
|
+
# Extract content from the streaming response format
|
120
|
+
# API returns: {"choices":[{"delta":{"content":"..."}}]}
|
121
|
+
if chunk["choices"]&.first&.dig("delta", "content")
|
122
|
+
print chunk["choices"][0]["delta"]["content"]
|
123
|
+
$stdout.flush
|
124
|
+
end
|
125
|
+
end
|
126
|
+
puts # Add newline at the end
|
127
|
+
else
|
128
|
+
# Non-streaming mode - collect full response and format
|
129
|
+
result = client.answer(args[:query], **answer_params)
|
130
|
+
output = Exa::CLI::Formatters::AnswerFormatter.format(result, output_format)
|
131
|
+
puts output
|
132
|
+
end
|
109
133
|
|
110
134
|
rescue Exa::ConfigurationError => e
|
111
135
|
$stderr.puts "Configuration error: #{e.message}"
|
data/exe/exa-ai-get-contents
CHANGED
@@ -11,8 +11,18 @@ def parse_args(args)
|
|
11
11
|
ids = []
|
12
12
|
api_key = nil
|
13
13
|
text = false
|
14
|
-
|
14
|
+
text_max_characters = nil
|
15
|
+
include_html_tags = false
|
15
16
|
summary = false
|
17
|
+
summary_query = nil
|
18
|
+
summary_schema = nil
|
19
|
+
subpages = nil
|
20
|
+
subpage_target = []
|
21
|
+
links = nil
|
22
|
+
image_links = nil
|
23
|
+
context = false
|
24
|
+
context_max_characters = nil
|
25
|
+
livecrawl_timeout = nil
|
16
26
|
output_format = nil
|
17
27
|
|
18
28
|
i = 0
|
@@ -25,10 +35,48 @@ def parse_args(args)
|
|
25
35
|
api_key = args[i]
|
26
36
|
when "--text"
|
27
37
|
text = true
|
28
|
-
|
29
|
-
|
38
|
+
i += 1
|
39
|
+
when "--text-max-characters"
|
40
|
+
i += 1
|
41
|
+
text_max_characters = args[i].to_i
|
42
|
+
when "--include-html-tags"
|
43
|
+
include_html_tags = true
|
44
|
+
i += 1
|
30
45
|
when "--summary"
|
31
46
|
summary = true
|
47
|
+
i += 1
|
48
|
+
when "--summary-query"
|
49
|
+
i += 1
|
50
|
+
summary_query = args[i]
|
51
|
+
when "--summary-schema"
|
52
|
+
i += 1
|
53
|
+
schema_arg = args[i]
|
54
|
+
summary_schema = if schema_arg.start_with?("@")
|
55
|
+
JSON.parse(File.read(schema_arg[1..]))
|
56
|
+
else
|
57
|
+
JSON.parse(schema_arg)
|
58
|
+
end
|
59
|
+
when "--subpages"
|
60
|
+
i += 1
|
61
|
+
subpages = args[i].to_i
|
62
|
+
when "--subpage-target"
|
63
|
+
i += 1
|
64
|
+
subpage_target << args[i]
|
65
|
+
when "--links"
|
66
|
+
i += 1
|
67
|
+
links = args[i].to_i
|
68
|
+
when "--image-links"
|
69
|
+
i += 1
|
70
|
+
image_links = args[i].to_i
|
71
|
+
when "--context"
|
72
|
+
context = true
|
73
|
+
i += 1
|
74
|
+
when "--context-max-characters"
|
75
|
+
i += 1
|
76
|
+
context_max_characters = args[i].to_i
|
77
|
+
when "--livecrawl-timeout"
|
78
|
+
i += 1
|
79
|
+
livecrawl_timeout = args[i].to_i
|
32
80
|
when "--output-format"
|
33
81
|
i += 1
|
34
82
|
output_format = args[i]
|
@@ -41,34 +89,130 @@ def parse_args(args)
|
|
41
89
|
ids_arg = arg
|
42
90
|
ids = ids_arg.include?(",") ? ids_arg.split(",").map(&:strip) : [ids_arg]
|
43
91
|
end
|
92
|
+
i += 1
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
{
|
97
|
+
ids: ids,
|
98
|
+
api_key: api_key,
|
99
|
+
text: text,
|
100
|
+
text_max_characters: text_max_characters,
|
101
|
+
include_html_tags: include_html_tags,
|
102
|
+
summary: summary,
|
103
|
+
summary_query: summary_query,
|
104
|
+
summary_schema: summary_schema,
|
105
|
+
subpages: subpages,
|
106
|
+
subpage_target: subpage_target,
|
107
|
+
links: links,
|
108
|
+
image_links: image_links,
|
109
|
+
context: context,
|
110
|
+
context_max_characters: context_max_characters,
|
111
|
+
livecrawl_timeout: livecrawl_timeout,
|
112
|
+
output_format: output_format
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
# Build contents parameters from extracted flags
|
117
|
+
def build_contents_params(args)
|
118
|
+
params = {}
|
119
|
+
|
120
|
+
# Text options
|
121
|
+
if args[:text]
|
122
|
+
if args[:text_max_characters] || args[:include_html_tags]
|
123
|
+
params[:text] = {}
|
124
|
+
params[:text][:max_characters] = args[:text_max_characters] if args[:text_max_characters]
|
125
|
+
params[:text][:include_html_tags] = args[:include_html_tags] if args[:include_html_tags]
|
126
|
+
else
|
127
|
+
params[:text] = true
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Summary options
|
132
|
+
if args[:summary]
|
133
|
+
if args[:summary_query] || args[:summary_schema]
|
134
|
+
params[:summary] = {}
|
135
|
+
params[:summary][:query] = args[:summary_query] if args[:summary_query]
|
136
|
+
params[:summary][:schema] = args[:summary_schema] if args[:summary_schema]
|
137
|
+
else
|
138
|
+
params[:summary] = true
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Context options
|
143
|
+
if args[:context]
|
144
|
+
if args[:context_max_characters]
|
145
|
+
params[:context] = { max_characters: args[:context_max_characters] }
|
146
|
+
else
|
147
|
+
params[:context] = true
|
44
148
|
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Subpages options
|
152
|
+
params[:subpages] = args[:subpages] if args[:subpages]
|
153
|
+
params[:subpage_target] = args[:subpage_target] if args[:subpage_target].any?
|
45
154
|
|
46
|
-
|
155
|
+
# Extras options
|
156
|
+
if args[:links] || args[:image_links]
|
157
|
+
params[:extras] = {}
|
158
|
+
params[:extras][:links] = args[:links] if args[:links]
|
159
|
+
params[:extras][:image_links] = args[:image_links] if args[:image_links]
|
47
160
|
end
|
48
161
|
|
49
|
-
|
162
|
+
# Livecrawl options
|
163
|
+
params[:livecrawl_timeout] = args[:livecrawl_timeout] if args[:livecrawl_timeout]
|
164
|
+
|
165
|
+
params.empty? ? nil : params
|
50
166
|
end
|
51
167
|
|
52
168
|
def print_help
|
53
|
-
puts
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
169
|
+
puts <<~HELP
|
170
|
+
Usage: exa-api get-contents <urls> [options]
|
171
|
+
|
172
|
+
Retrieve full page contents from URLs
|
173
|
+
|
174
|
+
Arguments:
|
175
|
+
urls Comma-separated list of URLs (required)
|
176
|
+
|
177
|
+
Options:
|
178
|
+
Text Extraction:
|
179
|
+
--text Include page text in response
|
180
|
+
--text-max-characters N Max characters for page text
|
181
|
+
--include-html-tags Include HTML tags in text extraction
|
182
|
+
|
183
|
+
Summary:
|
184
|
+
--summary Include AI-generated summary
|
185
|
+
--summary-query PROMPT Custom prompt for summary generation
|
186
|
+
--summary-schema FILE JSON schema for summary structure (@file syntax)
|
187
|
+
|
188
|
+
Context:
|
189
|
+
--context Format results as context for LLM RAG
|
190
|
+
--context-max-characters N Max characters for context string
|
191
|
+
|
192
|
+
Subpages:
|
193
|
+
--subpages N Number of subpages to crawl
|
194
|
+
--subpage-target PHRASE Subpage target phrases (repeatable)
|
195
|
+
|
196
|
+
Extras:
|
197
|
+
--links N Number of links to extract per result
|
198
|
+
--image-links N Number of image links to extract
|
199
|
+
|
200
|
+
Livecrawl:
|
201
|
+
--livecrawl-timeout N Timeout for livecrawling in milliseconds
|
202
|
+
|
203
|
+
General:
|
204
|
+
--api-key KEY Exa API key (or set EXA_API_KEY env var)
|
205
|
+
--output-format FMT Output format: json, pretty, or text (default: json)
|
206
|
+
--help, -h Show this help message
|
207
|
+
|
208
|
+
Examples:
|
209
|
+
exa-api get-contents 'https://example.com'
|
210
|
+
exa-api get-contents 'https://example.com' --text
|
211
|
+
exa-api get-contents 'https://example.com' --text --text-max-characters 3000 --include-html-tags
|
212
|
+
exa-api get-contents 'url1,url2' --summary --summary-query "Be terse"
|
213
|
+
exa-api get-contents 'https://example.com' --subpages 1 --subpage-target about
|
214
|
+
exa-api get-contents 'https://example.com' --links 5 --image-links 10
|
215
|
+
HELP
|
72
216
|
end
|
73
217
|
|
74
218
|
begin
|
@@ -77,9 +221,9 @@ begin
|
|
77
221
|
|
78
222
|
# Validate IDs
|
79
223
|
if options[:ids].empty?
|
80
|
-
puts "Error:
|
81
|
-
puts ""
|
82
|
-
puts "Run 'exa-api get-contents --help' for usage information."
|
224
|
+
$stderr.puts "Error: URLs argument required"
|
225
|
+
$stderr.puts ""
|
226
|
+
$stderr.puts "Run 'exa-api get-contents --help' for usage information."
|
83
227
|
exit 1
|
84
228
|
end
|
85
229
|
|
@@ -93,10 +237,8 @@ begin
|
|
93
237
|
client = Exa::CLI::Base.build_client(api_key)
|
94
238
|
|
95
239
|
# Build request parameters
|
96
|
-
params =
|
97
|
-
params
|
98
|
-
params[:highlights] = true if options[:highlights]
|
99
|
-
params[:summary] = true if options[:summary]
|
240
|
+
params = build_contents_params(options)
|
241
|
+
params ||= {}
|
100
242
|
|
101
243
|
# Call API
|
102
244
|
result = client.get_contents(options[:ids], **params)
|
@@ -104,11 +246,25 @@ begin
|
|
104
246
|
# Format and output
|
105
247
|
output = Exa::CLI::Formatters::ContentsFormatter.format(result, output_format)
|
106
248
|
puts output
|
249
|
+
rescue Exa::ConfigurationError => e
|
250
|
+
$stderr.puts "Configuration error: #{e.message}"
|
251
|
+
exit 1
|
252
|
+
rescue Exa::Unauthorized => e
|
253
|
+
$stderr.puts "Authentication error: #{e.message}"
|
254
|
+
$stderr.puts "Check your API key (set EXA_API_KEY or use --api-key)"
|
255
|
+
exit 1
|
256
|
+
rescue Exa::ClientError => e
|
257
|
+
$stderr.puts "Client error: #{e.message}"
|
258
|
+
exit 1
|
259
|
+
rescue Exa::ServerError => e
|
260
|
+
$stderr.puts "Server error: #{e.message}"
|
261
|
+
$stderr.puts "The Exa API may be experiencing issues. Please try again later."
|
262
|
+
exit 1
|
107
263
|
rescue Exa::Error => e
|
108
|
-
puts "Error: #{e.message}"
|
264
|
+
$stderr.puts "Error: #{e.message}"
|
109
265
|
exit 1
|
110
266
|
rescue StandardError => e
|
111
|
-
puts "Unexpected error: #{e.message}"
|
112
|
-
puts e.backtrace.first(5) if ENV["DEBUG"]
|
267
|
+
$stderr.puts "Unexpected error: #{e.message}"
|
268
|
+
$stderr.puts e.backtrace.first(5) if ENV["DEBUG"]
|
113
269
|
exit 1
|
114
270
|
end
|
data/exe/exa-ai-research-start
CHANGED
@@ -47,7 +47,7 @@ def parse_args(argv)
|
|
47
47
|
|
48
48
|
Options:
|
49
49
|
--instructions TEXT Research instructions (required)
|
50
|
-
--model MODEL
|
50
|
+
--model MODEL Research model: exa-research (default), exa-research-pro, exa-research-fast
|
51
51
|
--output-schema JSON JSON schema string for structured output
|
52
52
|
--wait Wait for task to complete (polls until done)
|
53
53
|
--events Include event log in output (only with --wait)
|
@@ -58,7 +58,7 @@ def parse_args(argv)
|
|
58
58
|
Examples:
|
59
59
|
exa-api research-start --instructions "Find Ruby performance tips"
|
60
60
|
exa-api research-start --instructions "Analyze AI trends" --wait --events
|
61
|
-
exa-api research-start --instructions "Summarize papers" --model
|
61
|
+
exa-api research-start --instructions "Summarize papers" --model exa-research-pro --wait
|
62
62
|
exa-api research-start --instructions "Find stats" --output-schema '{"type":"object"}'
|
63
63
|
HELP
|
64
64
|
exit 0
|
data/exe/exa-ai-search
CHANGED
@@ -24,21 +24,98 @@ def parse_args(argv)
|
|
24
24
|
when "--type"
|
25
25
|
args[:type] = argv[i + 1]
|
26
26
|
i += 2
|
27
|
+
when "--category"
|
28
|
+
category = argv[i + 1]
|
29
|
+
valid_categories = ["company", "research paper", "news", "pdf", "github", "tweet", "personal site", "linkedin profile", "financial report"]
|
30
|
+
unless valid_categories.include?(category)
|
31
|
+
$stderr.puts "Error: Category must be one of: #{valid_categories.map { |c| "\"#{c}\"" }.join(', ')}"
|
32
|
+
exit 1
|
33
|
+
end
|
34
|
+
args[:category] = category
|
35
|
+
i += 2
|
27
36
|
when "--include-domains"
|
28
37
|
args[:include_domains] = argv[i + 1].split(",").map(&:strip)
|
29
38
|
i += 2
|
30
39
|
when "--exclude-domains"
|
31
40
|
args[:exclude_domains] = argv[i + 1].split(",").map(&:strip)
|
32
41
|
i += 2
|
33
|
-
when "--use-autoprompt"
|
34
|
-
args[:use_autoprompt] = true
|
35
|
-
i += 1
|
36
42
|
when "--api-key"
|
37
43
|
args[:api_key] = argv[i + 1]
|
38
44
|
i += 2
|
39
45
|
when "--output-format"
|
40
46
|
args[:output_format] = argv[i + 1]
|
41
47
|
i += 2
|
48
|
+
when "--linkedin"
|
49
|
+
linkedin_type = argv[i + 1]
|
50
|
+
valid_types = ["company", "person", "all"]
|
51
|
+
unless valid_types.include?(linkedin_type)
|
52
|
+
$stderr.puts "Error: LinkedIn type must be one of: #{valid_types.join(', ')}"
|
53
|
+
exit 1
|
54
|
+
end
|
55
|
+
args[:linkedin] = linkedin_type
|
56
|
+
i += 2
|
57
|
+
when "--start-published-date"
|
58
|
+
args[:start_published_date] = argv[i + 1]
|
59
|
+
i += 2
|
60
|
+
when "--end-published-date"
|
61
|
+
args[:end_published_date] = argv[i + 1]
|
62
|
+
i += 2
|
63
|
+
when "--start-crawl-date"
|
64
|
+
args[:start_crawl_date] = argv[i + 1]
|
65
|
+
i += 2
|
66
|
+
when "--end-crawl-date"
|
67
|
+
args[:end_crawl_date] = argv[i + 1]
|
68
|
+
i += 2
|
69
|
+
when "--include-text"
|
70
|
+
args[:include_text] ||= []
|
71
|
+
args[:include_text] << argv[i + 1]
|
72
|
+
i += 2
|
73
|
+
when "--exclude-text"
|
74
|
+
args[:exclude_text] ||= []
|
75
|
+
args[:exclude_text] << argv[i + 1]
|
76
|
+
i += 2
|
77
|
+
when "--text"
|
78
|
+
args[:text] = true
|
79
|
+
i += 1
|
80
|
+
when "--text-max-characters"
|
81
|
+
args[:text_max_characters] = argv[i + 1].to_i
|
82
|
+
i += 2
|
83
|
+
when "--include-html-tags"
|
84
|
+
args[:include_html_tags] = true
|
85
|
+
i += 1
|
86
|
+
when "--summary"
|
87
|
+
args[:summary] = true
|
88
|
+
i += 1
|
89
|
+
when "--summary-query"
|
90
|
+
args[:summary_query] = argv[i + 1]
|
91
|
+
i += 2
|
92
|
+
when "--summary-schema"
|
93
|
+
schema_arg = argv[i + 1]
|
94
|
+
args[:summary_schema] = if schema_arg.start_with?("@")
|
95
|
+
JSON.parse(File.read(schema_arg[1..]))
|
96
|
+
else
|
97
|
+
JSON.parse(schema_arg)
|
98
|
+
end
|
99
|
+
i += 2
|
100
|
+
when "--context"
|
101
|
+
args[:context] = true
|
102
|
+
i += 1
|
103
|
+
when "--context-max-characters"
|
104
|
+
args[:context_max_characters] = argv[i + 1].to_i
|
105
|
+
i += 2
|
106
|
+
when "--subpages"
|
107
|
+
args[:subpages] = argv[i + 1].to_i
|
108
|
+
i += 2
|
109
|
+
when "--subpage-target"
|
110
|
+
args[:subpage_target] ||= []
|
111
|
+
args[:subpage_target] << argv[i + 1]
|
112
|
+
i += 2
|
113
|
+
when "--links"
|
114
|
+
args[:links] = argv[i + 1].to_i
|
115
|
+
i += 2
|
116
|
+
when "--image-links"
|
117
|
+
args[:image_links] = argv[i + 1].to_i
|
118
|
+
i += 2
|
42
119
|
when "--help", "-h"
|
43
120
|
puts <<~HELP
|
44
121
|
Usage: exa-api search QUERY [OPTIONS]
|
@@ -49,18 +126,49 @@ def parse_args(argv)
|
|
49
126
|
QUERY Search query (required)
|
50
127
|
|
51
128
|
Options:
|
52
|
-
--num-results N
|
53
|
-
--type TYPE
|
54
|
-
--
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
--
|
59
|
-
--
|
129
|
+
--num-results N Number of results to return (default: 10)
|
130
|
+
--type TYPE Search type: keyword, neural, fast, or auto (default: auto)
|
131
|
+
--category CAT Focus on specific data category
|
132
|
+
Options: "company", "research paper", "news", "pdf",
|
133
|
+
"github", "tweet", "personal site", "linkedin profile",
|
134
|
+
"financial report"
|
135
|
+
--include-domains D Comma-separated list of domains to include
|
136
|
+
--exclude-domains D Comma-separated list of domains to exclude
|
137
|
+
--start-published-date DATE Filter by published date (ISO 8601 format)
|
138
|
+
--end-published-date DATE Filter by published date (ISO 8601 format)
|
139
|
+
--start-crawl-date DATE Filter by crawl date (ISO 8601 format)
|
140
|
+
--end-crawl-date DATE Filter by crawl date (ISO 8601 format)
|
141
|
+
--include-text PHRASE Include results with exact phrase (repeatable)
|
142
|
+
--exclude-text PHRASE Exclude results with exact phrase (repeatable)
|
143
|
+
|
144
|
+
Content Extraction:
|
145
|
+
--text Include full webpage text
|
146
|
+
--text-max-characters N Max characters for webpage text
|
147
|
+
--include-html-tags Include HTML tags in text extraction
|
148
|
+
--summary Include AI-generated summary
|
149
|
+
--summary-query PROMPT Custom prompt for summary generation
|
150
|
+
--summary-schema FILE JSON schema for summary structure (@file syntax)
|
151
|
+
--context Format results as context for LLM RAG
|
152
|
+
--context-max-characters N Max characters for context string
|
153
|
+
--subpages N Number of subpages to crawl
|
154
|
+
--subpage-target PHRASE Subpage target phrases (repeatable)
|
155
|
+
--links N Number of links to extract per result
|
156
|
+
--image-links N Number of image links to extract
|
157
|
+
|
158
|
+
General Options:
|
159
|
+
--linkedin TYPE Search LinkedIn: company, person, or all
|
160
|
+
--api-key KEY Exa API key (or set EXA_API_KEY env var)
|
161
|
+
--output-format FMT Output format: json, pretty, or text (default: json)
|
162
|
+
--help, -h Show this help message
|
60
163
|
|
61
164
|
Examples:
|
62
165
|
exa-api search "ruby programming"
|
63
166
|
exa-api search "machine learning" --num-results 5 --type keyword
|
167
|
+
exa-api search "Latest LLM research" --category "research paper"
|
168
|
+
exa-api search "AI startups" --category company
|
169
|
+
exa-api search "Anthropic" --linkedin company
|
170
|
+
exa-api search "Dario Amodei" --linkedin person
|
171
|
+
exa-api search "AI" --linkedin all
|
64
172
|
exa-api search "AI research" --include-domains arxiv.org,scholar.google.com
|
65
173
|
exa-api search "tutorials" --output-format pretty
|
66
174
|
HELP
|
@@ -75,6 +183,55 @@ def parse_args(argv)
|
|
75
183
|
args
|
76
184
|
end
|
77
185
|
|
186
|
+
# Build contents parameter from extracted flags
|
187
|
+
def build_contents(args)
|
188
|
+
contents = {}
|
189
|
+
|
190
|
+
# Text options
|
191
|
+
if args[:text]
|
192
|
+
if args[:text_max_characters] || args[:include_html_tags]
|
193
|
+
contents[:text] = {}
|
194
|
+
contents[:text][:max_characters] = args[:text_max_characters] if args[:text_max_characters]
|
195
|
+
contents[:text][:include_html_tags] = args[:include_html_tags] if args[:include_html_tags]
|
196
|
+
else
|
197
|
+
contents[:text] = true
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Summary options
|
202
|
+
if args[:summary]
|
203
|
+
if args[:summary_query] || args[:summary_schema]
|
204
|
+
contents[:summary] = {}
|
205
|
+
contents[:summary][:query] = args[:summary_query] if args[:summary_query]
|
206
|
+
contents[:summary][:schema] = args[:summary_schema] if args[:summary_schema]
|
207
|
+
else
|
208
|
+
contents[:summary] = true
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Context options
|
213
|
+
if args[:context]
|
214
|
+
if args[:context_max_characters]
|
215
|
+
contents[:context] = { max_characters: args[:context_max_characters] }
|
216
|
+
else
|
217
|
+
contents[:context] = true
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# Subpages options
|
222
|
+
contents[:subpages] = args[:subpages] if args[:subpages]
|
223
|
+
contents[:subpage_target] = args[:subpage_target] if args[:subpage_target]
|
224
|
+
|
225
|
+
# Extras options
|
226
|
+
if args[:links] || args[:image_links]
|
227
|
+
contents[:extras] = {}
|
228
|
+
contents[:extras][:links] = args[:links] if args[:links]
|
229
|
+
contents[:extras][:image_links] = args[:image_links] if args[:image_links]
|
230
|
+
end
|
231
|
+
|
232
|
+
contents.empty? ? nil : contents
|
233
|
+
end
|
234
|
+
|
78
235
|
# Main execution
|
79
236
|
begin
|
80
237
|
args = parse_args(ARGV)
|
@@ -97,14 +254,31 @@ begin
|
|
97
254
|
|
98
255
|
# Prepare search parameters
|
99
256
|
search_params = {}
|
100
|
-
search_params[:
|
257
|
+
search_params[:numResults] = args[:num_results] if args[:num_results]
|
101
258
|
search_params[:type] = args[:type] if args[:type]
|
102
|
-
search_params[:
|
103
|
-
search_params[:
|
104
|
-
search_params[:
|
259
|
+
search_params[:category] = args[:category] if args[:category]
|
260
|
+
search_params[:includeDomains] = args[:include_domains] if args[:include_domains]
|
261
|
+
search_params[:excludeDomains] = args[:exclude_domains] if args[:exclude_domains]
|
262
|
+
search_params[:start_published_date] = args[:start_published_date] if args[:start_published_date]
|
263
|
+
search_params[:end_published_date] = args[:end_published_date] if args[:end_published_date]
|
264
|
+
search_params[:start_crawl_date] = args[:start_crawl_date] if args[:start_crawl_date]
|
265
|
+
search_params[:end_crawl_date] = args[:end_crawl_date] if args[:end_crawl_date]
|
266
|
+
search_params[:include_text] = args[:include_text] if args[:include_text]
|
267
|
+
search_params[:exclude_text] = args[:exclude_text] if args[:exclude_text]
|
268
|
+
contents = build_contents(args)
|
269
|
+
search_params.merge!(contents) if contents
|
105
270
|
|
106
|
-
# Execute search
|
107
|
-
result =
|
271
|
+
# Execute search based on LinkedIn type
|
272
|
+
result = case args[:linkedin]
|
273
|
+
when "company"
|
274
|
+
client.linkedin_company(args[:query], **search_params)
|
275
|
+
when "person"
|
276
|
+
client.linkedin_person(args[:query], **search_params)
|
277
|
+
when "all"
|
278
|
+
client.search(args[:query], includeDomains: ["linkedin.com"], **search_params)
|
279
|
+
else
|
280
|
+
client.search(args[:query], **search_params)
|
281
|
+
end
|
108
282
|
|
109
283
|
# Format and output result
|
110
284
|
output = Exa::CLI::Formatters::SearchFormatter.format(result, output_format)
|
data/lib/exa/client.rb
CHANGED
@@ -71,6 +71,21 @@ module Exa
|
|
71
71
|
Services::Answer.new(connection, query: query, **options).call
|
72
72
|
end
|
73
73
|
|
74
|
+
# Stream AI-generated answers to a query
|
75
|
+
#
|
76
|
+
# Returns partial answer chunks as they are generated by the API.
|
77
|
+
#
|
78
|
+
# @param query [String] Question or query
|
79
|
+
# @param options [Hash] Answer options
|
80
|
+
# @option options [Boolean] :text Include full text content (default: false)
|
81
|
+
# @option options [Hash] :output_schema JSON schema for structured output
|
82
|
+
# @yield [chunk] Yields each answer chunk as it arrives
|
83
|
+
# @yieldparam chunk [Hash] Partial answer data with {"answer" => "text"}
|
84
|
+
# @return [void]
|
85
|
+
def answer_stream(query, **options, &block)
|
86
|
+
Services::AnswerStream.new(connection, query: query, **options).call(&block)
|
87
|
+
end
|
88
|
+
|
74
89
|
# Start an asynchronous research task
|
75
90
|
#
|
76
91
|
# @param params [Hash] Research parameters
|
@@ -107,6 +122,32 @@ module Exa
|
|
107
122
|
Services::Context.new(connection, query: query, **params).call
|
108
123
|
end
|
109
124
|
|
125
|
+
# Search for LinkedIn company pages
|
126
|
+
#
|
127
|
+
# Convenience method that restricts search to LinkedIn company profiles
|
128
|
+
# using keyword search for precise name matching.
|
129
|
+
#
|
130
|
+
# @param query [String] Company name to search
|
131
|
+
# @param params [Hash] Additional search parameters
|
132
|
+
# @option params [Integer] :numResults Number of results to return
|
133
|
+
# @return [Resources::SearchResult] LinkedIn company results
|
134
|
+
def linkedin_company(query, **params)
|
135
|
+
search(query, type: "keyword", includeDomains: ["linkedin.com/company"], **params)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Search for LinkedIn profiles
|
139
|
+
#
|
140
|
+
# Convenience method that restricts search to LinkedIn individual profiles
|
141
|
+
# using keyword search for precise name matching.
|
142
|
+
#
|
143
|
+
# @param query [String] Person name to search
|
144
|
+
# @param params [Hash] Additional search parameters
|
145
|
+
# @option params [Integer] :numResults Number of results to return
|
146
|
+
# @return [Resources::SearchResult] LinkedIn profile results
|
147
|
+
def linkedin_person(query, **params)
|
148
|
+
search(query, type: "keyword", includeDomains: ["linkedin.com/in"], **params)
|
149
|
+
end
|
150
|
+
|
110
151
|
private
|
111
152
|
|
112
153
|
def connection
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module Exa
|
4
|
+
module Services
|
5
|
+
class AnswerStream
|
6
|
+
def initialize(connection, **params)
|
7
|
+
@connection = connection
|
8
|
+
@params = params.merge(stream: true)
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(&block)
|
12
|
+
raise ArgumentError, "block required for streaming" unless block_given?
|
13
|
+
|
14
|
+
# Use instance variable to track buffer across on_data callbacks
|
15
|
+
@buffer = ""
|
16
|
+
|
17
|
+
# Configure the request to stream chunks via on_data callback
|
18
|
+
@connection.post("/answer", @params) do |req|
|
19
|
+
req.options.on_data = proc do |chunk|
|
20
|
+
# Add chunk to buffer and process complete SSE events
|
21
|
+
@buffer += chunk
|
22
|
+
process_sse_buffer(&block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Process any remaining data in buffer after stream ends
|
27
|
+
process_remaining_buffer(&block) if @buffer.length.positive?
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def process_sse_buffer
|
33
|
+
# Extract and process complete SSE events (separated by \n\n)
|
34
|
+
# If buffer ends with \n\n, all parts are complete.
|
35
|
+
# Otherwise, keep the last part in buffer for next chunk.
|
36
|
+
|
37
|
+
return if @buffer.empty?
|
38
|
+
|
39
|
+
parts = @buffer.split("\n\n")
|
40
|
+
|
41
|
+
# When split by a delimiter, if the string ends with the delimiter,
|
42
|
+
# split doesn't add a trailing empty string. So we need to track
|
43
|
+
# whether the buffer ended with \n\n to know if the last part is incomplete.
|
44
|
+
if @buffer.end_with?("\n\n")
|
45
|
+
# All parts are complete, clear buffer
|
46
|
+
complete_parts = parts
|
47
|
+
@buffer = ""
|
48
|
+
else
|
49
|
+
# Last part is incomplete, keep it for next chunk
|
50
|
+
complete_parts = parts[0...-1]
|
51
|
+
@buffer = parts.last || ""
|
52
|
+
end
|
53
|
+
|
54
|
+
# Process all complete events
|
55
|
+
complete_parts.each do |event|
|
56
|
+
next if event.empty?
|
57
|
+
|
58
|
+
lines = event.split("\n")
|
59
|
+
lines.each do |line|
|
60
|
+
if line.start_with?("data: ")
|
61
|
+
json_str = line.sub(/^data: /, "").strip
|
62
|
+
begin
|
63
|
+
data = JSON.parse(json_str)
|
64
|
+
yield(data)
|
65
|
+
rescue JSON::ParserError
|
66
|
+
# Skip lines that aren't valid JSON
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def process_remaining_buffer
|
74
|
+
# Process any remaining incomplete buffer
|
75
|
+
lines = @buffer.split("\n")
|
76
|
+
lines.each do |line|
|
77
|
+
if line.start_with?("data: ")
|
78
|
+
json_str = line.sub(/^data: /, "").strip
|
79
|
+
begin
|
80
|
+
data = JSON.parse(json_str)
|
81
|
+
yield(data)
|
82
|
+
rescue JSON::ParserError
|
83
|
+
# Skip lines that aren't valid JSON
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Exa
|
4
|
+
module Services
|
5
|
+
# Converts Ruby parameter names (snake_case) to API format (camelCase)
|
6
|
+
# Handles both simple parameters and nested content parameters
|
7
|
+
class ParameterConverter
|
8
|
+
def self.convert(params)
|
9
|
+
new.convert(params)
|
10
|
+
end
|
11
|
+
|
12
|
+
def convert(params)
|
13
|
+
converted = {}
|
14
|
+
contents = {}
|
15
|
+
|
16
|
+
params.each do |key, value|
|
17
|
+
if content_key?(key)
|
18
|
+
contents[convert_content_key(key)] = convert_content_value(key, value)
|
19
|
+
else
|
20
|
+
converted[convert_key(key)] = value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
converted[:contents] = contents if contents.any?
|
25
|
+
converted
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def convert_key(key)
|
31
|
+
case key
|
32
|
+
when :start_published_date then :startPublishedDate
|
33
|
+
when :end_published_date then :endPublishedDate
|
34
|
+
when :start_crawl_date then :startCrawlDate
|
35
|
+
when :end_crawl_date then :endCrawlDate
|
36
|
+
when :include_text then :includeText
|
37
|
+
when :exclude_text then :excludeText
|
38
|
+
else
|
39
|
+
key
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def content_key?(key)
|
44
|
+
%i[text summary context subpages subpage_target extras].include?(key)
|
45
|
+
end
|
46
|
+
|
47
|
+
def convert_content_key(key)
|
48
|
+
case key
|
49
|
+
when :subpage_target then :subpageTarget
|
50
|
+
else
|
51
|
+
key
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def convert_content_value(key, value)
|
56
|
+
case key
|
57
|
+
when :text
|
58
|
+
if value.is_a?(Hash)
|
59
|
+
convert_hash_value(value, text_hash_mappings)
|
60
|
+
else
|
61
|
+
value
|
62
|
+
end
|
63
|
+
when :summary
|
64
|
+
if value.is_a?(Hash)
|
65
|
+
convert_hash_value(value, summary_hash_mappings)
|
66
|
+
else
|
67
|
+
value
|
68
|
+
end
|
69
|
+
when :context
|
70
|
+
if value.is_a?(Hash)
|
71
|
+
convert_hash_value(value, context_hash_mappings)
|
72
|
+
else
|
73
|
+
value
|
74
|
+
end
|
75
|
+
when :extras
|
76
|
+
if value.is_a?(Hash)
|
77
|
+
convert_hash_value(value, extras_hash_mappings)
|
78
|
+
else
|
79
|
+
value
|
80
|
+
end
|
81
|
+
else
|
82
|
+
value
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def convert_hash_value(hash, mappings)
|
87
|
+
converted = {}
|
88
|
+
hash.each do |k, v|
|
89
|
+
converted_key = mappings[k] || k
|
90
|
+
converted[converted_key] = v
|
91
|
+
end
|
92
|
+
converted
|
93
|
+
end
|
94
|
+
|
95
|
+
def text_hash_mappings
|
96
|
+
{
|
97
|
+
max_characters: :maxCharacters,
|
98
|
+
include_html_tags: :includeHtmlTags
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
def summary_hash_mappings
|
103
|
+
{
|
104
|
+
query: :query,
|
105
|
+
schema: :schema
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
def context_hash_mappings
|
110
|
+
{
|
111
|
+
max_characters: :maxCharacters
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
def extras_hash_mappings
|
116
|
+
{
|
117
|
+
image_links: :imageLinks
|
118
|
+
}
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
data/lib/exa/services/search.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "parameter_converter"
|
4
|
+
|
3
5
|
module Exa
|
4
6
|
module Services
|
5
7
|
class Search
|
@@ -9,7 +11,7 @@ module Exa
|
|
9
11
|
end
|
10
12
|
|
11
13
|
def call
|
12
|
-
response = @connection.post("/search", @params)
|
14
|
+
response = @connection.post("/search", ParameterConverter.convert(@params))
|
13
15
|
body = response.body
|
14
16
|
|
15
17
|
Resources::SearchResult.new(
|
data/lib/exa/version.rb
CHANGED
data/lib/exa.rb
CHANGED
@@ -18,6 +18,7 @@ require_relative "exa/services/research_get"
|
|
18
18
|
require_relative "exa/services/research_start"
|
19
19
|
require_relative "exa/services/research_list"
|
20
20
|
require_relative "exa/services/answer"
|
21
|
+
require_relative "exa/services/answer_stream"
|
21
22
|
require_relative "exa/services/context"
|
22
23
|
require_relative "exa/client"
|
23
24
|
require_relative "exa/cli/base"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: exa-ai
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Benjamin Jackson
|
@@ -23,6 +23,20 @@ dependencies:
|
|
23
23
|
- - "~>"
|
24
24
|
- !ruby/object:Gem::Version
|
25
25
|
version: '2.0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: ld-eventsource
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '2.0'
|
26
40
|
- !ruby/object:Gem::Dependency
|
27
41
|
name: minitest
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
@@ -139,9 +153,11 @@ files:
|
|
139
153
|
- lib/exa/resources/research_task.rb
|
140
154
|
- lib/exa/resources/search_result.rb
|
141
155
|
- lib/exa/services/answer.rb
|
156
|
+
- lib/exa/services/answer_stream.rb
|
142
157
|
- lib/exa/services/context.rb
|
143
158
|
- lib/exa/services/find_similar.rb
|
144
159
|
- lib/exa/services/get_contents.rb
|
160
|
+
- lib/exa/services/parameter_converter.rb
|
145
161
|
- lib/exa/services/research_get.rb
|
146
162
|
- lib/exa/services/research_list.rb
|
147
163
|
- lib/exa/services/research_start.rb
|