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
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# typed: strict
|
|
4
|
+
|
|
5
|
+
module Exa
|
|
6
|
+
module Endpoints
|
|
7
|
+
module Imports
|
|
8
|
+
VALID_FORMATS = %w[csv webset].freeze
|
|
9
|
+
VALID_ENTITY_TYPES = %w[company person article research_paper custom].freeze
|
|
10
|
+
|
|
11
|
+
# Creates a new Import to upload data into Websets
|
|
12
|
+
#
|
|
13
|
+
# Imports can be used to:
|
|
14
|
+
# - Enrich: Enhance data with additional information
|
|
15
|
+
# - Search: Query data using Websets' agentic search
|
|
16
|
+
# - Exclude: Prevent duplicate results in searches
|
|
17
|
+
#
|
|
18
|
+
# Once created, upload your data to the returned uploadUrl.
|
|
19
|
+
#
|
|
20
|
+
# @param size [Integer] File size in bytes (max 50MB)
|
|
21
|
+
# @param count [Integer] Number of records to import
|
|
22
|
+
# @param format [String] Import format: "csv" or "webset"
|
|
23
|
+
# @param entity [Hash] Entity type configuration
|
|
24
|
+
# @option entity [String] :type "company", "person", "article", "research_paper", or "custom"
|
|
25
|
+
# @param title [String, nil] Title of the import
|
|
26
|
+
# @param metadata [Hash, nil] Custom metadata
|
|
27
|
+
# @param csv [Hash, nil] CSV-specific parameters (identifier column index)
|
|
28
|
+
#
|
|
29
|
+
# @return [Exa::Resources::Import] Created Import with uploadUrl
|
|
30
|
+
#
|
|
31
|
+
# @example Create a CSV import
|
|
32
|
+
# import = client.create_import(
|
|
33
|
+
# size: 1024,
|
|
34
|
+
# count: 100,
|
|
35
|
+
# format: "csv",
|
|
36
|
+
# entity: { type: "company" },
|
|
37
|
+
# title: "Q4 Leads",
|
|
38
|
+
# csv: { identifier: 1 }
|
|
39
|
+
# )
|
|
40
|
+
# puts "Upload to: #{import.upload_url}"
|
|
41
|
+
# puts "Valid until: #{import.upload_valid_until}"
|
|
42
|
+
def create_import(size:, count:, format: "csv", entity:, title: nil, metadata: nil, csv: nil)
|
|
43
|
+
validate_import_params!(size, format, entity)
|
|
44
|
+
|
|
45
|
+
params = {
|
|
46
|
+
size: size,
|
|
47
|
+
count: count,
|
|
48
|
+
format: format,
|
|
49
|
+
entity: entity
|
|
50
|
+
}
|
|
51
|
+
params[:title] = title if title
|
|
52
|
+
params[:metadata] = metadata if metadata
|
|
53
|
+
params[:csv] = csv if csv
|
|
54
|
+
|
|
55
|
+
response = websets_post("/imports", params)
|
|
56
|
+
|
|
57
|
+
Resources::Import.new(
|
|
58
|
+
Utils::ParameterConverter.from_api_response(response)
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Gets an Import by ID
|
|
63
|
+
#
|
|
64
|
+
# @param import_id [String] Import ID
|
|
65
|
+
# @return [Exa::Resources::Import] The Import
|
|
66
|
+
def get_import(import_id)
|
|
67
|
+
raise InvalidRequestError, "import_id must be a non-empty string" if !import_id.is_a?(String) || import_id.empty?
|
|
68
|
+
|
|
69
|
+
response = websets_get("/imports/#{import_id}")
|
|
70
|
+
|
|
71
|
+
Resources::Import.new(
|
|
72
|
+
Utils::ParameterConverter.from_api_response(response)
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Lists all Imports
|
|
77
|
+
#
|
|
78
|
+
# @param cursor [String, nil] Cursor for pagination
|
|
79
|
+
# @param limit [Integer, nil] Number of results per page
|
|
80
|
+
#
|
|
81
|
+
# @return [Exa::Resources::ImportListResponse] Paginated list of Imports
|
|
82
|
+
def list_imports(cursor: nil, limit: nil)
|
|
83
|
+
params = {}
|
|
84
|
+
params[:cursor] = cursor if cursor
|
|
85
|
+
params[:limit] = limit if limit
|
|
86
|
+
|
|
87
|
+
response = websets_get("/imports", params)
|
|
88
|
+
|
|
89
|
+
Resources::ImportListResponse.new(
|
|
90
|
+
Utils::ParameterConverter.from_api_response(response)
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Updates an Import
|
|
95
|
+
#
|
|
96
|
+
# @param import_id [String] Import ID
|
|
97
|
+
# @param title [String, nil] Updated title
|
|
98
|
+
# @param metadata [Hash, nil] Updated metadata
|
|
99
|
+
#
|
|
100
|
+
# @return [Exa::Resources::Import] Updated Import
|
|
101
|
+
def update_import(import_id, title: nil, metadata: nil)
|
|
102
|
+
raise InvalidRequestError, "import_id must be a non-empty string" if !import_id.is_a?(String) || import_id.empty?
|
|
103
|
+
|
|
104
|
+
params = {}
|
|
105
|
+
params[:title] = title if title
|
|
106
|
+
params[:metadata] = metadata if metadata
|
|
107
|
+
|
|
108
|
+
response = websets_patch("/imports/#{import_id}", params)
|
|
109
|
+
|
|
110
|
+
Resources::Import.new(
|
|
111
|
+
Utils::ParameterConverter.from_api_response(response)
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Deletes an Import
|
|
116
|
+
#
|
|
117
|
+
# @param import_id [String] Import ID
|
|
118
|
+
# @return [Hash] Deletion confirmation
|
|
119
|
+
def delete_import(import_id)
|
|
120
|
+
raise InvalidRequestError, "import_id must be a non-empty string" if !import_id.is_a?(String) || import_id.empty?
|
|
121
|
+
|
|
122
|
+
websets_delete("/imports/#{import_id}")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def validate_import_params!(size, format, entity)
|
|
128
|
+
if size > 50_000_000
|
|
129
|
+
raise InvalidRequestError, "size must be 50MB or less"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
unless VALID_FORMATS.include?(format)
|
|
133
|
+
raise InvalidRequestError, "Invalid format: #{format}. Valid formats: #{VALID_FORMATS.join(", ")}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
if entity && entity[:type]
|
|
137
|
+
entity_type = entity[:type].to_s.downcase.tr(" ", "_")
|
|
138
|
+
unless VALID_ENTITY_TYPES.include?(entity_type)
|
|
139
|
+
raise InvalidRequestError, "Invalid entity type: #{entity[:type]}. Valid types: #{VALID_ENTITY_TYPES.join(", ")}"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# typed: strict
|
|
4
|
+
|
|
5
|
+
module Exa
|
|
6
|
+
module Endpoints
|
|
7
|
+
module Monitors
|
|
8
|
+
VALID_BEHAVIOR_TYPES = %w[search refresh].freeze
|
|
9
|
+
|
|
10
|
+
# Creates a new Monitor to continuously keep Websets updated
|
|
11
|
+
#
|
|
12
|
+
# Monitors automatically run on your defined schedule to ensure Websets
|
|
13
|
+
# stay current without manual intervention:
|
|
14
|
+
# - Find new content via search operations
|
|
15
|
+
# - Update existing content via refresh operations
|
|
16
|
+
# - Automated scheduling using cron expressions
|
|
17
|
+
#
|
|
18
|
+
# @param webset_id [String] The Webset ID to monitor
|
|
19
|
+
# @param cadence [Hash] Schedule configuration
|
|
20
|
+
# @option cadence [String] :cron Cron expression for scheduling
|
|
21
|
+
# @option cadence [String] :timezone Timezone (default: "Etc/UTC")
|
|
22
|
+
# @param behavior [Hash] Behavior when monitor runs
|
|
23
|
+
# @option behavior [String] :type "search" or "refresh"
|
|
24
|
+
# @option behavior [Hash] :config Configuration for the behavior
|
|
25
|
+
# @param metadata [Hash] Custom metadata key-value pairs
|
|
26
|
+
#
|
|
27
|
+
# @return [Exa::Resources::Monitor] Created Monitor
|
|
28
|
+
#
|
|
29
|
+
# @example Create a daily search monitor
|
|
30
|
+
# monitor = client.create_monitor(
|
|
31
|
+
# webset_id: "webset_abc123",
|
|
32
|
+
# cadence: { cron: "0 9 * * *", timezone: "America/New_York" },
|
|
33
|
+
# behavior: {
|
|
34
|
+
# type: "search",
|
|
35
|
+
# config: {
|
|
36
|
+
# count: 50,
|
|
37
|
+
# query: "AI news today",
|
|
38
|
+
# entity: { type: "article" },
|
|
39
|
+
# behavior: "append"
|
|
40
|
+
# }
|
|
41
|
+
# }
|
|
42
|
+
# )
|
|
43
|
+
def create_monitor(webset_id:, cadence:, behavior:, metadata: nil)
|
|
44
|
+
validate_monitor_params!(webset_id, cadence, behavior)
|
|
45
|
+
|
|
46
|
+
params = {
|
|
47
|
+
websetId: webset_id,
|
|
48
|
+
cadence: cadence,
|
|
49
|
+
behavior: behavior
|
|
50
|
+
}
|
|
51
|
+
params[:metadata] = metadata if metadata
|
|
52
|
+
|
|
53
|
+
response = websets_post("/monitors", params)
|
|
54
|
+
|
|
55
|
+
Resources::Monitor.new(
|
|
56
|
+
Utils::ParameterConverter.from_api_response(response)
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Gets a Monitor by ID
|
|
61
|
+
#
|
|
62
|
+
# @param monitor_id [String] Monitor ID
|
|
63
|
+
# @return [Exa::Resources::Monitor] The Monitor
|
|
64
|
+
#
|
|
65
|
+
# @example Get a monitor
|
|
66
|
+
# monitor = client.get_monitor("monitor_abc123")
|
|
67
|
+
# puts "Next run: #{monitor.next_run_at}"
|
|
68
|
+
def get_monitor(monitor_id)
|
|
69
|
+
raise InvalidRequestError, "monitor_id must be a non-empty string" if !monitor_id.is_a?(String) || monitor_id.empty?
|
|
70
|
+
|
|
71
|
+
response = websets_get("/monitors/#{monitor_id}")
|
|
72
|
+
|
|
73
|
+
Resources::Monitor.new(
|
|
74
|
+
Utils::ParameterConverter.from_api_response(response)
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Lists all Monitors
|
|
79
|
+
#
|
|
80
|
+
# @param cursor [String, nil] Cursor for pagination
|
|
81
|
+
# @param limit [Integer, nil] Number of results per page
|
|
82
|
+
# @param webset_id [String, nil] Filter by Webset ID
|
|
83
|
+
#
|
|
84
|
+
# @return [Exa::Resources::MonitorListResponse] Paginated list of Monitors
|
|
85
|
+
#
|
|
86
|
+
# @example List monitors
|
|
87
|
+
# response = client.list_monitors(limit: 10)
|
|
88
|
+
# response.data.each { |m| puts "#{m.id}: #{m.status}" }
|
|
89
|
+
def list_monitors(cursor: nil, limit: nil, webset_id: nil)
|
|
90
|
+
params = {}
|
|
91
|
+
params[:cursor] = cursor if cursor
|
|
92
|
+
params[:limit] = limit if limit
|
|
93
|
+
params[:websetId] = webset_id if webset_id
|
|
94
|
+
|
|
95
|
+
response = websets_get("/monitors", params)
|
|
96
|
+
|
|
97
|
+
Resources::MonitorListResponse.new(
|
|
98
|
+
Utils::ParameterConverter.from_api_response(response)
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Updates a Monitor
|
|
103
|
+
#
|
|
104
|
+
# @param monitor_id [String] Monitor ID
|
|
105
|
+
# @param cadence [Hash, nil] Updated schedule configuration
|
|
106
|
+
# @param behavior [Hash, nil] Updated behavior configuration
|
|
107
|
+
# @param status [String, nil] "enabled" or "disabled"
|
|
108
|
+
# @param metadata [Hash, nil] Updated metadata
|
|
109
|
+
#
|
|
110
|
+
# @return [Exa::Resources::Monitor] Updated Monitor
|
|
111
|
+
#
|
|
112
|
+
# @example Disable a monitor
|
|
113
|
+
# monitor = client.update_monitor("monitor_abc123", status: "disabled")
|
|
114
|
+
def update_monitor(monitor_id, cadence: nil, behavior: nil, status: nil, metadata: nil)
|
|
115
|
+
raise InvalidRequestError, "monitor_id must be a non-empty string" if !monitor_id.is_a?(String) || monitor_id.empty?
|
|
116
|
+
|
|
117
|
+
params = {}
|
|
118
|
+
params[:cadence] = cadence if cadence
|
|
119
|
+
params[:behavior] = behavior if behavior
|
|
120
|
+
params[:status] = status if status
|
|
121
|
+
params[:metadata] = metadata if metadata
|
|
122
|
+
|
|
123
|
+
response = websets_patch("/monitors/#{monitor_id}", params)
|
|
124
|
+
|
|
125
|
+
Resources::Monitor.new(
|
|
126
|
+
Utils::ParameterConverter.from_api_response(response)
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Deletes a Monitor
|
|
131
|
+
#
|
|
132
|
+
# @param monitor_id [String] Monitor ID
|
|
133
|
+
# @return [Hash] Deletion confirmation
|
|
134
|
+
#
|
|
135
|
+
# @example Delete a monitor
|
|
136
|
+
# client.delete_monitor("monitor_abc123")
|
|
137
|
+
def delete_monitor(monitor_id)
|
|
138
|
+
raise InvalidRequestError, "monitor_id must be a non-empty string" if !monitor_id.is_a?(String) || monitor_id.empty?
|
|
139
|
+
|
|
140
|
+
websets_delete("/monitors/#{monitor_id}")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Gets a specific Monitor Run
|
|
144
|
+
#
|
|
145
|
+
# @param monitor_id [String] Monitor ID
|
|
146
|
+
# @param run_id [String] Run ID
|
|
147
|
+
# @return [Exa::Resources::MonitorRun] The Monitor Run
|
|
148
|
+
def get_monitor_run(monitor_id, run_id)
|
|
149
|
+
raise InvalidRequestError, "monitor_id must be a non-empty string" if !monitor_id.is_a?(String) || monitor_id.empty?
|
|
150
|
+
raise InvalidRequestError, "run_id must be a non-empty string" if !run_id.is_a?(String) || run_id.empty?
|
|
151
|
+
|
|
152
|
+
response = websets_get("/monitors/#{monitor_id}/runs/#{run_id}")
|
|
153
|
+
|
|
154
|
+
Resources::MonitorRun.new(
|
|
155
|
+
Utils::ParameterConverter.from_api_response(response)
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Lists runs for a Monitor
|
|
160
|
+
#
|
|
161
|
+
# @param monitor_id [String] Monitor ID
|
|
162
|
+
# @param cursor [String, nil] Cursor for pagination
|
|
163
|
+
# @param limit [Integer, nil] Number of results per page
|
|
164
|
+
#
|
|
165
|
+
# @return [Exa::Resources::MonitorRunListResponse] Paginated list of runs
|
|
166
|
+
def list_monitor_runs(monitor_id, cursor: nil, limit: nil)
|
|
167
|
+
raise InvalidRequestError, "monitor_id must be a non-empty string" if !monitor_id.is_a?(String) || monitor_id.empty?
|
|
168
|
+
|
|
169
|
+
params = {}
|
|
170
|
+
params[:cursor] = cursor if cursor
|
|
171
|
+
params[:limit] = limit if limit
|
|
172
|
+
|
|
173
|
+
response = websets_get("/monitors/#{monitor_id}/runs", params)
|
|
174
|
+
|
|
175
|
+
Resources::MonitorRunListResponse.new(
|
|
176
|
+
Utils::ParameterConverter.from_api_response(response)
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
def validate_monitor_params!(webset_id, cadence, behavior)
|
|
183
|
+
raise InvalidRequestError, "webset_id must be a non-empty string" if !webset_id.is_a?(String) || webset_id.empty?
|
|
184
|
+
raise InvalidRequestError, "cadence is required" unless cadence
|
|
185
|
+
raise InvalidRequestError, "behavior is required" unless behavior
|
|
186
|
+
|
|
187
|
+
if behavior[:type] && !VALID_BEHAVIOR_TYPES.include?(behavior[:type])
|
|
188
|
+
raise InvalidRequestError, "Invalid behavior type: #{behavior[:type]}. Valid types: #{VALID_BEHAVIOR_TYPES.join(", ")}"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# typed: strict
|
|
4
|
+
|
|
5
|
+
module Exa
|
|
6
|
+
module Endpoints
|
|
7
|
+
module Research
|
|
8
|
+
VALID_MODELS = %w[exa-research-fast exa-research exa-research-pro].freeze
|
|
9
|
+
VALID_STATUSES = %w[pending running completed canceled failed].freeze
|
|
10
|
+
|
|
11
|
+
# Create an asynchronous research task
|
|
12
|
+
#
|
|
13
|
+
# Creates a research task that explores the web, gathers sources, synthesizes
|
|
14
|
+
# findings, and returns results with citations. Can generate:
|
|
15
|
+
# 1. Structured JSON matching an outputSchema you provide
|
|
16
|
+
# 2. A detailed markdown report when no schema is provided
|
|
17
|
+
#
|
|
18
|
+
# The API responds immediately with a research_id for polling completion status.
|
|
19
|
+
#
|
|
20
|
+
# @param instructions [String] Instructions for what to research (max 4096 chars)
|
|
21
|
+
# @param model [String] Research model: "exa-research-fast", "exa-research", "exa-research-pro"
|
|
22
|
+
# @param output_schema [Hash] JSON Schema to enforce structured output
|
|
23
|
+
#
|
|
24
|
+
# @return [Exa::Resources::ResearchTask] Research task with ID for polling
|
|
25
|
+
#
|
|
26
|
+
# @raise [Exa::InvalidRequestError] if parameters are invalid
|
|
27
|
+
#
|
|
28
|
+
# @example Create a basic research task
|
|
29
|
+
# task = client.create_research(
|
|
30
|
+
# instructions: "Summarize the latest developments in AI safety research"
|
|
31
|
+
# )
|
|
32
|
+
# puts "Task ID: #{task.research_id}"
|
|
33
|
+
# puts "Status: #{task.status}"
|
|
34
|
+
#
|
|
35
|
+
# @example Create research with structured output
|
|
36
|
+
# schema = {
|
|
37
|
+
# type: "object",
|
|
38
|
+
# properties: {
|
|
39
|
+
# companies: {
|
|
40
|
+
# type: "array",
|
|
41
|
+
# items: { type: "string" }
|
|
42
|
+
# },
|
|
43
|
+
# summary: { type: "string" }
|
|
44
|
+
# }
|
|
45
|
+
# }
|
|
46
|
+
# task = client.create_research(
|
|
47
|
+
# instructions: "Find the top 5 AI startups in 2024",
|
|
48
|
+
# model: "exa-research-pro",
|
|
49
|
+
# output_schema: schema
|
|
50
|
+
# )
|
|
51
|
+
def create_research(instructions:, model: "exa-research", output_schema: nil)
|
|
52
|
+
validate_research_params!(instructions, model)
|
|
53
|
+
|
|
54
|
+
params = {
|
|
55
|
+
instructions: instructions,
|
|
56
|
+
model: model
|
|
57
|
+
}
|
|
58
|
+
params[:outputSchema] = output_schema if output_schema
|
|
59
|
+
|
|
60
|
+
response = post("/research/v1", params)
|
|
61
|
+
|
|
62
|
+
Resources::ResearchTask.new(
|
|
63
|
+
Utils::ParameterConverter.from_api_response(response)
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get the status and results of a research task
|
|
68
|
+
#
|
|
69
|
+
# Poll this endpoint to check research progress. When status is "completed",
|
|
70
|
+
# the output field contains the research results.
|
|
71
|
+
#
|
|
72
|
+
# @param research_id [String] The research task ID
|
|
73
|
+
#
|
|
74
|
+
# @return [Exa::Resources::ResearchTask] Research task with current status and output
|
|
75
|
+
#
|
|
76
|
+
# @raise [Exa::NotFoundError] if research task not found
|
|
77
|
+
#
|
|
78
|
+
# @example Poll for completion
|
|
79
|
+
# task = client.get_research(research_id)
|
|
80
|
+
# case task.status
|
|
81
|
+
# when "completed"
|
|
82
|
+
# puts task.output
|
|
83
|
+
# when "running"
|
|
84
|
+
# puts "Progress: #{task.operations_completed}/#{task.operations_total}"
|
|
85
|
+
# when "failed"
|
|
86
|
+
# puts "Error: #{task.error_message}"
|
|
87
|
+
# end
|
|
88
|
+
def get_research(research_id)
|
|
89
|
+
raise InvalidRequestError, "research_id must be a non-empty string" if !research_id.is_a?(String) || research_id.empty?
|
|
90
|
+
|
|
91
|
+
response = get("/research/v1/#{research_id}")
|
|
92
|
+
|
|
93
|
+
Resources::ResearchTask.new(
|
|
94
|
+
Utils::ParameterConverter.from_api_response(response)
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# List all research tasks
|
|
99
|
+
#
|
|
100
|
+
# @param cursor [String, nil] Cursor for pagination
|
|
101
|
+
# @param limit [Integer, nil] Number of results per page
|
|
102
|
+
#
|
|
103
|
+
# @return [Exa::Resources::ResearchListResponse] Paginated list of research tasks
|
|
104
|
+
#
|
|
105
|
+
# @example List recent research tasks
|
|
106
|
+
# response = client.list_research(limit: 10)
|
|
107
|
+
# response.data.each { |task| puts "#{task.research_id}: #{task.status}" }
|
|
108
|
+
def list_research(cursor: nil, limit: nil)
|
|
109
|
+
params = {}
|
|
110
|
+
params[:cursor] = cursor if cursor
|
|
111
|
+
params[:limit] = limit if limit
|
|
112
|
+
|
|
113
|
+
response = get("/research/v1", params)
|
|
114
|
+
|
|
115
|
+
Resources::ResearchListResponse.new(
|
|
116
|
+
Utils::ParameterConverter.from_api_response(response)
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Cancel a running research task
|
|
121
|
+
#
|
|
122
|
+
# @param research_id [String] The research task ID to cancel
|
|
123
|
+
#
|
|
124
|
+
# @return [Exa::Resources::ResearchTask] The canceled research task
|
|
125
|
+
#
|
|
126
|
+
# @raise [Exa::NotFoundError] if research task not found
|
|
127
|
+
#
|
|
128
|
+
# @example Cancel a research task
|
|
129
|
+
# task = client.cancel_research(research_id)
|
|
130
|
+
# puts "Canceled: #{task.status}" # => "canceled"
|
|
131
|
+
def cancel_research(research_id)
|
|
132
|
+
raise InvalidRequestError, "research_id must be a non-empty string" if !research_id.is_a?(String) || research_id.empty?
|
|
133
|
+
|
|
134
|
+
response = post("/research/v1/#{research_id}/cancel", {})
|
|
135
|
+
|
|
136
|
+
Resources::ResearchTask.new(
|
|
137
|
+
Utils::ParameterConverter.from_api_response(response)
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
# @param instructions [String] Research instructions
|
|
144
|
+
# @param model [String] Research model
|
|
145
|
+
def validate_research_params!(instructions, model)
|
|
146
|
+
raise InvalidRequestError, "instructions must be a non-empty string" if !instructions.is_a?(String) || instructions.empty?
|
|
147
|
+
|
|
148
|
+
if instructions.length > 4096
|
|
149
|
+
raise InvalidRequestError, "instructions must be 4096 characters or less"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
unless VALID_MODELS.include?(model)
|
|
153
|
+
raise InvalidRequestError, "Invalid model: #{model}. Valid models: #{VALID_MODELS.join(", ")}"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# typed: strict
|
|
4
|
+
|
|
5
|
+
module Exa
|
|
6
|
+
module Endpoints
|
|
7
|
+
module Search
|
|
8
|
+
VALID_SEARCH_TYPES = %i[neural auto fast deep].freeze
|
|
9
|
+
VALID_CATEGORIES = %i[
|
|
10
|
+
people company research_paper news pdf github tweet personal_site financial_report
|
|
11
|
+
].freeze
|
|
12
|
+
VALID_LIVECRAWL_OPTIONS = %i[never fallback preferred always].freeze
|
|
13
|
+
|
|
14
|
+
# Performs an intelligent web search using Exa's embeddings-based model
|
|
15
|
+
#
|
|
16
|
+
# @param query [String] The search query string
|
|
17
|
+
# @param type [Symbol] Search type: :neural, :auto (default), :fast, or :deep
|
|
18
|
+
# @param additional_queries [Array<String>] Additional query variations for deep search
|
|
19
|
+
# @param category [Symbol] Data category to focus on (e.g., :people, :company, :research_paper)
|
|
20
|
+
# @param num_results [Integer] Number of results to return (max 100)
|
|
21
|
+
# @param include_domains [Array<String>] Domains to include in results
|
|
22
|
+
# @param exclude_domains [Array<String>] Domains to exclude from results
|
|
23
|
+
# @param start_crawl_date [String, Time] Results crawled after this date (ISO 8601)
|
|
24
|
+
# @param end_crawl_date [String, Time] Results crawled before this date (ISO 8601)
|
|
25
|
+
# @param start_published_date [String, Time] Results published after this date (ISO 8601)
|
|
26
|
+
# @param end_published_date [String, Time] Results published before this date (ISO 8601)
|
|
27
|
+
# @param include_text [Array<String>] Strings that must be present in page text
|
|
28
|
+
# @param exclude_text [Array<String>] Strings that must not be present in page text
|
|
29
|
+
# @param country [String] Two-letter ISO country code (e.g., "US")
|
|
30
|
+
# @param text [Boolean, Hash] Return text content. Hash for options: { max_characters: 1000 }
|
|
31
|
+
# @param highlights [Boolean, Hash] Return highlights. Hash for options: { num_sentences: 3, highlight_query: "..." }
|
|
32
|
+
# @param summary [Boolean, Hash] Return summary. Hash for options: { query: "..." }
|
|
33
|
+
# @param context [Boolean, Integer] Return context string for LLM. Integer for max characters
|
|
34
|
+
# @param moderation [Boolean] Enable content moderation
|
|
35
|
+
# @param livecrawl [Symbol] Livecrawl option: :never, :fallback, :preferred, :always
|
|
36
|
+
# @param livecrawl_timeout [Integer] Livecrawl timeout in milliseconds
|
|
37
|
+
# @param subpages [Integer] Number of subpages to crawl
|
|
38
|
+
# @param subpage_target [String, Array<String>] Terms to find specific subpages
|
|
39
|
+
#
|
|
40
|
+
# @return [Exa::Resources::SearchResponse] Search results
|
|
41
|
+
#
|
|
42
|
+
# @raise [Exa::InvalidRequestError] if parameters are invalid
|
|
43
|
+
# @raise [Exa::AuthenticationError] if API key is invalid
|
|
44
|
+
# @raise [Exa::RateLimitError] if rate limit is exceeded
|
|
45
|
+
#
|
|
46
|
+
# @example Basic search
|
|
47
|
+
# client.search("Latest developments in LLMs")
|
|
48
|
+
#
|
|
49
|
+
# @example Search with content extraction
|
|
50
|
+
# client.search("AI research papers", text: true, num_results: 20)
|
|
51
|
+
#
|
|
52
|
+
# @example Deep search with filters
|
|
53
|
+
# client.search(
|
|
54
|
+
# "Machine learning startups",
|
|
55
|
+
# type: :deep,
|
|
56
|
+
# category: :company,
|
|
57
|
+
# include_domains: ["linkedin.com", "crunchbase.com"],
|
|
58
|
+
# start_published_date: "2024-01-01T00:00:00.000Z"
|
|
59
|
+
# )
|
|
60
|
+
def search(query, **options)
|
|
61
|
+
validate_search_options!(options)
|
|
62
|
+
|
|
63
|
+
params = build_search_params(query, options)
|
|
64
|
+
response = post("/search", params)
|
|
65
|
+
|
|
66
|
+
Resources::SearchResponse.new(
|
|
67
|
+
Utils::ParameterConverter.from_api_response(response)
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# @param options [Hash] Search options to validate
|
|
74
|
+
# @raise [Exa::InvalidRequestError] if options are invalid
|
|
75
|
+
def validate_search_options!(options)
|
|
76
|
+
if options[:type] && !VALID_SEARCH_TYPES.include?(options[:type])
|
|
77
|
+
raise InvalidRequestError, "Invalid search type: #{options[:type]}. Valid types: #{VALID_SEARCH_TYPES.join(", ")}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if options[:category] && !VALID_CATEGORIES.include?(options[:category])
|
|
81
|
+
raise InvalidRequestError, "Invalid category: #{options[:category]}. Valid categories: #{VALID_CATEGORIES.join(", ")}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if options[:livecrawl] && !VALID_LIVECRAWL_OPTIONS.include?(options[:livecrawl])
|
|
85
|
+
raise InvalidRequestError, "Invalid livecrawl option: #{options[:livecrawl]}. Valid options: #{VALID_LIVECRAWL_OPTIONS.join(", ")}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if options[:num_results] && (options[:num_results] < 1 || options[:num_results] > 100)
|
|
89
|
+
raise InvalidRequestError, "num_results must be between 1 and 100"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if options[:additional_queries] && options[:type] != :deep
|
|
93
|
+
raise InvalidRequestError, "additional_queries is only supported with type: :deep"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @param query [String] Search query
|
|
98
|
+
# @param options [Hash] Search options
|
|
99
|
+
# @return [Hash] API-formatted parameters
|
|
100
|
+
def build_search_params(query, options)
|
|
101
|
+
params = { query: query }
|
|
102
|
+
|
|
103
|
+
params[:type] = options[:type].to_s if options[:type]
|
|
104
|
+
params[:additionalQueries] = options[:additional_queries] if options[:additional_queries]
|
|
105
|
+
params[:category] = format_category(options[:category]) if options[:category]
|
|
106
|
+
params[:numResults] = options[:num_results] if options[:num_results]
|
|
107
|
+
params[:userLocation] = options[:country] if options[:country]
|
|
108
|
+
params[:moderation] = options[:moderation] if options.key?(:moderation)
|
|
109
|
+
|
|
110
|
+
add_domain_filters!(params, options)
|
|
111
|
+
add_date_filters!(params, options)
|
|
112
|
+
add_text_filters!(params, options)
|
|
113
|
+
add_content_options!(params, options)
|
|
114
|
+
|
|
115
|
+
params
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @param category [Symbol] Category symbol
|
|
119
|
+
# @return [String] API-formatted category
|
|
120
|
+
def format_category(category)
|
|
121
|
+
category.to_s.tr("_", " ")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @param params [Hash] Parameters to modify
|
|
125
|
+
# @param options [Hash] Source options
|
|
126
|
+
def add_domain_filters!(params, options)
|
|
127
|
+
params[:includeDomains] = options[:include_domains] if options[:include_domains]
|
|
128
|
+
params[:excludeDomains] = options[:exclude_domains] if options[:exclude_domains]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @param params [Hash] Parameters to modify
|
|
132
|
+
# @param options [Hash] Source options
|
|
133
|
+
def add_date_filters!(params, options)
|
|
134
|
+
params[:startCrawlDate] = format_date(options[:start_crawl_date]) if options[:start_crawl_date]
|
|
135
|
+
params[:endCrawlDate] = format_date(options[:end_crawl_date]) if options[:end_crawl_date]
|
|
136
|
+
params[:startPublishedDate] = format_date(options[:start_published_date]) if options[:start_published_date]
|
|
137
|
+
params[:endPublishedDate] = format_date(options[:end_published_date]) if options[:end_published_date]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @param params [Hash] Parameters to modify
|
|
141
|
+
# @param options [Hash] Source options
|
|
142
|
+
def add_text_filters!(params, options)
|
|
143
|
+
params[:includeText] = options[:include_text] if options[:include_text]
|
|
144
|
+
params[:excludeText] = options[:exclude_text] if options[:exclude_text]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# @param params [Hash] Parameters to modify
|
|
148
|
+
# @param options [Hash] Source options
|
|
149
|
+
def add_content_options!(params, options)
|
|
150
|
+
params[:text] = format_content_option(options[:text]) if options.key?(:text)
|
|
151
|
+
params[:highlights] = format_highlights_option(options[:highlights]) if options.key?(:highlights)
|
|
152
|
+
params[:summary] = format_summary_option(options[:summary]) if options.key?(:summary)
|
|
153
|
+
|
|
154
|
+
if options.key?(:context)
|
|
155
|
+
params[:context] = options[:context].is_a?(Integer) ? { maxCharacters: options[:context] } : options[:context]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
params[:livecrawl] = options[:livecrawl].to_s if options[:livecrawl]
|
|
159
|
+
params[:livecrawlTimeout] = options[:livecrawl_timeout] if options[:livecrawl_timeout]
|
|
160
|
+
params[:subpages] = options[:subpages] if options[:subpages]
|
|
161
|
+
params[:subpageTarget] = options[:subpage_target] if options[:subpage_target]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# @param option [Boolean, Hash] Text option
|
|
165
|
+
# @return [Boolean, Hash] Formatted option
|
|
166
|
+
def format_content_option(option)
|
|
167
|
+
return option if option.is_a?(TrueClass) || option.is_a?(FalseClass)
|
|
168
|
+
|
|
169
|
+
Utils::ParameterConverter.to_api_params(option)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# @param option [Boolean, Hash] Highlights option
|
|
173
|
+
# @return [Boolean, Hash] Formatted option
|
|
174
|
+
def format_highlights_option(option)
|
|
175
|
+
return option if option.is_a?(TrueClass) || option.is_a?(FalseClass)
|
|
176
|
+
|
|
177
|
+
Utils::ParameterConverter.to_api_params(option)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# @param option [Boolean, Hash] Summary option
|
|
181
|
+
# @return [Boolean, Hash] Formatted option
|
|
182
|
+
def format_summary_option(option)
|
|
183
|
+
return option if option.is_a?(TrueClass) || option.is_a?(FalseClass)
|
|
184
|
+
|
|
185
|
+
Utils::ParameterConverter.to_api_params(option)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# @param value [Time, Date, String, nil] Date value
|
|
189
|
+
# @return [String, nil] ISO 8601 formatted string
|
|
190
|
+
def format_date(value)
|
|
191
|
+
Utils::ParameterConverter.format_date(value)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|