exa-ai 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8604fda006df666f4162cf8d76e83ef294c9a24d8a759e23c926e48479b97ca
4
- data.tar.gz: 2bf1a0a4f601e8da648f4eb4e72e66a40312c93ec6d0950c1cf04a9207fd70d6
3
+ metadata.gz: ffa1108ddff0ff951dc18d12eeef57f74020a4c0425916a394fac95e4699fef5
4
+ data.tar.gz: 43c40cc61a7bee139dc3c179fe9d11c40b04dd506d53c22f88e312705ae9dfe3
5
5
  SHA512:
6
- metadata.gz: ad102f8404ed8195bb345843b76b8287da5a29d2d50988fb0bd6bcbf5d1681c10344c1e95dab9891c7aebdd5f3989b9216ed15e32389e395f5471ddc166ccda8
7
- data.tar.gz: 8ec9279adc183c8a101b359aa4213828559aac9c434f16a0128f409c939d489905f3a59d601885e9438d63409426e675473e8df345db7f8e6a56b6a84d0cac8a
6
+ metadata.gz: 5745df2cf4664fff7b8a507fb82662cd1e448a51dba7a0a0ac30485d624ac1bb3624e85b6086a6bd511a9485636b0d5a9804c561e9476106679a7458572fcf62
7
+ data.tar.gz: 637d5150a3b0d192b1c68288c83c4e0ad13827d4d60ac9deb4a974397ab9b0c2ab86dd9281f33bc11dc5e1fd7ba06b22daa4c8d0f80c8927c108946e4c8ffe46
data/exe/exa-ai-search CHANGED
@@ -15,7 +15,8 @@ def print_help
15
15
 
16
16
  Options:
17
17
  --num-results N Number of results to return (default: 10)
18
- --type TYPE Search type: fast, deep, keyword, or auto (default: fast)
18
+ --type TYPE Search type: auto, neural, fast, deep, deep-reasoning,
19
+ or instant (default: auto)
19
20
  --category CAT Focus on specific data category
20
21
  Options: "company", "research paper", "news", "pdf",
21
22
  "github", "tweet", "personal site", "financial report",
@@ -42,6 +43,19 @@ def print_help
42
43
  --subpage-target PHRASE Subpage target phrases (repeatable)
43
44
  --links N Number of links to extract per result
44
45
  --image-links N Number of image links to extract
46
+ --highlights Include text highlights in results
47
+ --highlights-max-characters N Max characters per highlight
48
+ --highlights-num-sentences N Number of sentences per highlight
49
+ --highlights-per-url N Number of highlights per URL
50
+ --highlights-query QUERY Custom query for highlight extraction
51
+ --livecrawl MODE Livecrawl mode: always, fallback, never, auto, or preferred
52
+ --livecrawl-timeout N Livecrawl timeout in milliseconds
53
+ --max-age-hours N Maximum age of results in hours
54
+
55
+ Search Options:
56
+ --additional-queries QUERY Additional search queries (repeatable)
57
+ --output-schema JSON JSON schema for output structure (@file syntax)
58
+ --user-location CODE User location (ISO country code)
45
59
 
46
60
  General Options:
47
61
  --api-key KEY Exa API key (or set EXA_API_KEY env var)
@@ -105,6 +119,25 @@ def build_contents(args)
105
119
  contents[:extras][:image_links] = args[:image_links] if args[:image_links]
106
120
  end
107
121
 
122
+ # Highlights options
123
+ if args[:highlights]
124
+ highlight_opts = %i[highlights_max_characters highlights_num_sentences highlights_per_url highlights_query]
125
+ if highlight_opts.any? { |k| args[k] }
126
+ contents[:highlights] = {}
127
+ contents[:highlights][:max_characters] = args[:highlights_max_characters] if args[:highlights_max_characters]
128
+ contents[:highlights][:num_sentences] = args[:highlights_num_sentences] if args[:highlights_num_sentences]
129
+ contents[:highlights][:highlights_per_url] = args[:highlights_per_url] if args[:highlights_per_url]
130
+ contents[:highlights][:query] = args[:highlights_query] if args[:highlights_query]
131
+ else
132
+ contents[:highlights] = true
133
+ end
134
+ end
135
+
136
+ # Livecrawl options
137
+ contents[:livecrawl] = args[:livecrawl] if args[:livecrawl]
138
+ contents[:livecrawl_timeout] = args[:livecrawl_timeout] if args[:livecrawl_timeout]
139
+ contents[:max_age_hours] = args[:max_age_hours] if args[:max_age_hours]
140
+
108
141
  contents.empty? ? nil : contents
109
142
  end
110
143
 
@@ -130,17 +163,20 @@ begin
130
163
 
131
164
  # Prepare search parameters
132
165
  search_params = {}
133
- search_params[:numResults] = args[:num_results] if args[:num_results]
166
+ search_params[:num_results] = args[:num_results] if args[:num_results]
134
167
  search_params[:type] = args[:type] if args[:type]
135
168
  search_params[:category] = args[:category] if args[:category]
136
- search_params[:includeDomains] = args[:include_domains] if args[:include_domains]
137
- search_params[:excludeDomains] = args[:exclude_domains] if args[:exclude_domains]
169
+ search_params[:include_domains] = args[:include_domains] if args[:include_domains]
170
+ search_params[:exclude_domains] = args[:exclude_domains] if args[:exclude_domains]
138
171
  search_params[:start_published_date] = args[:start_published_date] if args[:start_published_date]
139
172
  search_params[:end_published_date] = args[:end_published_date] if args[:end_published_date]
140
173
  search_params[:start_crawl_date] = args[:start_crawl_date] if args[:start_crawl_date]
141
174
  search_params[:end_crawl_date] = args[:end_crawl_date] if args[:end_crawl_date]
142
175
  search_params[:include_text] = args[:include_text] if args[:include_text]
143
176
  search_params[:exclude_text] = args[:exclude_text] if args[:exclude_text]
177
+ search_params[:additional_queries] = args[:additional_queries] if args[:additional_queries]
178
+ search_params[:output_schema] = args[:output_schema] if args[:output_schema]
179
+ search_params[:user_location] = args[:user_location] if args[:user_location]
144
180
  contents = build_contents(args)
145
181
  search_params.merge!(contents) if contents
146
182
 
@@ -61,6 +61,9 @@ def parse_args(argv)
61
61
  when "--external-id"
62
62
  args[:external_id] = argv[i + 1]
63
63
  i += 2
64
+ when "--title"
65
+ args[:title] = argv[i + 1]
66
+ i += 2
64
67
  when "--metadata"
65
68
  args[:metadata] = parse_json_or_file(argv[i + 1])
66
69
  i += 2
@@ -93,6 +96,7 @@ def parse_args(argv)
93
96
  --exclude JSON Sources to exclude from searches (supports @file.json)
94
97
  Format: [{"source":"import|webset","id":"..."}]
95
98
  --external-id ID External identifier for the webset
99
+ --title TITLE Title for the webset
96
100
  --metadata JSON Custom metadata (supports @file.json)
97
101
  Format: {"key":"value"}
98
102
  --wait Wait for webset to reach idle status
@@ -185,6 +189,7 @@ begin
185
189
  webset_params[:enrichments] = args[:enrichments] if args[:enrichments]
186
190
  webset_params[:exclude] = args[:exclude] if args[:exclude]
187
191
  webset_params[:externalId] = args[:external_id] if args[:external_id]
192
+ webset_params[:title] = args[:title] if args[:title]
188
193
  webset_params[:metadata] = args[:metadata] if args[:metadata]
189
194
 
190
195
  # Create webset
@@ -6,6 +6,7 @@ require "json"
6
6
 
7
7
  # Parse arguments
8
8
  webset_id = nil
9
+ title = nil
9
10
  metadata = nil
10
11
  api_key = nil
11
12
  output_format = "json"
@@ -45,6 +46,8 @@ args = ARGV.dup
45
46
  while args.any?
46
47
  arg = args.shift
47
48
  case arg
49
+ when "--title"
50
+ title = args.shift
48
51
  when "--metadata"
49
52
  metadata = parse_json_or_file(args.shift)
50
53
  when "--api-key"
@@ -55,12 +58,13 @@ while args.any?
55
58
  puts <<~HELP
56
59
  Usage: exa-ai webset-update <webset_id> [OPTIONS]
57
60
 
58
- Update a webset's metadata
61
+ Update a webset's title and/or metadata
59
62
 
60
63
  Arguments:
61
64
  webset_id ID of the webset to update (required)
62
65
 
63
66
  Options:
67
+ --title TITLE Title for the webset
64
68
  --metadata JSON Custom metadata to update (supports @file.json)
65
69
  --api-key KEY Exa API key (or set EXA_API_KEY env var)
66
70
  --output-format FMT Output format: json, pretty, or text (default: json)
@@ -68,6 +72,8 @@ while args.any?
68
72
 
69
73
  Examples:
70
74
  exa-ai webset-update ws_abc123 --metadata '{"project":"Q1-2025"}'
75
+ exa-ai webset-update ws_abc123 --title "Q1 Research"
76
+ exa-ai webset-update ws_abc123 --title "Q1 Research" --metadata '{"project":"Q1-2025"}'
71
77
  exa-ai webset-update ws_abc123 --metadata @metadata.json
72
78
  exa-ai webset-update ws_abc123 --metadata '{"tags":["important"]}' --output-format pretty
73
79
  HELP
@@ -91,12 +97,6 @@ if webset_id.nil?
91
97
  exit 1
92
98
  end
93
99
 
94
- if metadata.nil?
95
- $stderr.puts "Error: --metadata is required"
96
- $stderr.puts "Try 'exa-ai webset-update --help' for more information"
97
- exit 1
98
- end
99
-
100
100
  begin
101
101
  # Resolve API key and format
102
102
  api_key = Exa::CLI::Base.resolve_api_key(api_key)
@@ -106,7 +106,10 @@ begin
106
106
  client = Exa::CLI::Base.build_client(api_key)
107
107
 
108
108
  # Update webset
109
- webset = client.update_webset(webset_id, metadata: metadata)
109
+ update_params = {}
110
+ update_params[:title] = title if title
111
+ update_params[:metadata] = metadata if metadata
112
+ webset = client.update_webset(webset_id, **update_params)
110
113
 
111
114
  # Format and output
112
115
  output = Exa::CLI::Formatters::WebsetFormatter.format(webset, output_format)
@@ -38,6 +38,7 @@ module Exa
38
38
  lines = []
39
39
  lines << "Webset: #{webset.id}"
40
40
  lines << "Status: #{webset.status}"
41
+ lines << "Title: #{webset.title}" if webset.title
41
42
  lines << "Created: #{webset.created_at}" if webset.created_at
42
43
  lines << "Updated: #{webset.updated_at}" if webset.updated_at
43
44
 
@@ -61,6 +62,7 @@ module Exa
61
62
  collection.data.each do |ws|
62
63
  lines << "\n #{ws['id']}"
63
64
  lines << " Status: #{ws['status']}"
65
+ lines << " Title: #{ws['title']}" if ws['title']
64
66
  lines << " Created: #{ws['createdAt']}" if ws['createdAt']
65
67
  end
66
68
  lines.join("\n")
@@ -3,7 +3,8 @@
3
3
  module Exa
4
4
  module CLI
5
5
  class SearchParser
6
- VALID_SEARCH_TYPES = ["fast", "deep", "keyword", "auto"].freeze
6
+ VALID_SEARCH_TYPES = ["auto", "neural", "fast", "deep", "deep-reasoning", "instant"].freeze
7
+ VALID_LIVECRAWL_MODES = ["always", "fallback", "never", "auto", "preferred"].freeze
7
8
  VALID_CATEGORIES = [
8
9
  "company", "research paper", "news", "pdf", "github",
9
10
  "tweet", "personal site", "financial report", "people"
@@ -123,6 +124,47 @@ module Exa
123
124
  when "--image-links"
124
125
  @args[:image_links] = @argv[i + 1].to_i
125
126
  i += 2
127
+ when "--highlights"
128
+ @args[:highlights] = true
129
+ i += 1
130
+ when "--highlights-max-characters"
131
+ @args[:highlights_max_characters] = @argv[i + 1].to_i
132
+ i += 2
133
+ when "--highlights-num-sentences"
134
+ @args[:highlights_num_sentences] = @argv[i + 1].to_i
135
+ i += 2
136
+ when "--highlights-per-url"
137
+ @args[:highlights_per_url] = @argv[i + 1].to_i
138
+ i += 2
139
+ when "--highlights-query"
140
+ @args[:highlights_query] = @argv[i + 1]
141
+ i += 2
142
+ when "--livecrawl"
143
+ livecrawl = @argv[i + 1]
144
+ validate_livecrawl(livecrawl)
145
+ @args[:livecrawl] = livecrawl
146
+ i += 2
147
+ when "--livecrawl-timeout"
148
+ @args[:livecrawl_timeout] = @argv[i + 1].to_i
149
+ i += 2
150
+ when "--max-age-hours"
151
+ @args[:max_age_hours] = @argv[i + 1].to_i
152
+ i += 2
153
+ when "--additional-queries"
154
+ @args[:additional_queries] ||= []
155
+ @args[:additional_queries] << @argv[i + 1]
156
+ i += 2
157
+ when "--output-schema"
158
+ schema_arg = @argv[i + 1]
159
+ @args[:output_schema] = if schema_arg.start_with?("@")
160
+ JSON.parse(File.read(schema_arg[1..]))
161
+ else
162
+ JSON.parse(schema_arg)
163
+ end
164
+ i += 2
165
+ when "--user-location"
166
+ @args[:user_location] = @argv[i + 1]
167
+ i += 2
126
168
  else
127
169
  query_parts << arg
128
170
  i += 1
@@ -142,6 +184,12 @@ module Exa
142
184
  raise ArgumentError, "Search type must be one of: #{VALID_SEARCH_TYPES.join(', ')}"
143
185
  end
144
186
 
187
+ def validate_livecrawl(mode)
188
+ return if VALID_LIVECRAWL_MODES.include?(mode)
189
+
190
+ raise ArgumentError, "Livecrawl mode must be one of: #{VALID_LIVECRAWL_MODES.join(', ')}"
191
+ end
192
+
145
193
  def validate_category(category)
146
194
  return if VALID_CATEGORIES.include?(category)
147
195
 
data/lib/exa/client.rb CHANGED
@@ -158,10 +158,11 @@ module Exa
158
158
  Services::Websets::Cancel.new(connection, id: id).call
159
159
  end
160
160
 
161
- # Update a webset's metadata
161
+ # Update a webset's title and/or metadata
162
162
  #
163
163
  # @param id [String] Webset ID
164
164
  # @param params [Hash] Update parameters
165
+ # @option params [String] :title Title for the webset
165
166
  # @option params [Hash] :metadata Metadata to update
166
167
  # @return [Resources::Webset] The updated webset
167
168
  def update_webset(id, **params)
@@ -11,9 +11,10 @@ module Exa
11
11
  :search_type,
12
12
  :context,
13
13
  :cost_dollars,
14
+ :output,
14
15
  keyword_init: true
15
16
  )
16
- def initialize(results:, request_id: nil, resolved_search_type: nil, search_type: nil, context: nil, cost_dollars: nil)
17
+ def initialize(results:, request_id: nil, resolved_search_type: nil, search_type: nil, context: nil, cost_dollars: nil, output: nil)
17
18
  super
18
19
  freeze
19
20
  end
@@ -36,18 +36,26 @@ module Exa
36
36
  when :include_text then :includeText
37
37
  when :exclude_text then :excludeText
38
38
  when :external_id then :externalId
39
+ when :additional_queries then :additionalQueries
40
+ when :output_schema then :outputSchema
41
+ when :user_location then :userLocation
42
+ when :num_results then :numResults
43
+ when :include_domains then :includeDomains
44
+ when :exclude_domains then :excludeDomains
39
45
  else
40
46
  key
41
47
  end
42
48
  end
43
49
 
44
50
  def content_key?(key)
45
- %i[text summary context subpages subpage_target extras].include?(key)
51
+ %i[text summary context subpages subpage_target extras highlights livecrawl livecrawl_timeout max_age_hours].include?(key)
46
52
  end
47
53
 
48
54
  def convert_content_key(key)
49
55
  case key
50
56
  when :subpage_target then :subpageTarget
57
+ when :livecrawl_timeout then :livecrawlTimeout
58
+ when :max_age_hours then :maxAgeHours
51
59
  else
52
60
  key
53
61
  end
@@ -79,6 +87,12 @@ module Exa
79
87
  else
80
88
  value
81
89
  end
90
+ when :highlights
91
+ if value.is_a?(Hash)
92
+ convert_hash_value(value, highlights_hash_mappings)
93
+ else
94
+ value
95
+ end
82
96
  else
83
97
  value
84
98
  end
@@ -96,7 +110,9 @@ module Exa
96
110
  def text_hash_mappings
97
111
  {
98
112
  max_characters: :maxCharacters,
99
- include_html_tags: :includeHtmlTags
113
+ include_html_tags: :includeHtmlTags,
114
+ include_sections: :includeSections,
115
+ exclude_sections: :excludeSections
100
116
  }
101
117
  end
102
118
 
@@ -118,6 +134,15 @@ module Exa
118
134
  image_links: :imageLinks
119
135
  }
120
136
  end
137
+
138
+ def highlights_hash_mappings
139
+ {
140
+ max_characters: :maxCharacters,
141
+ num_sentences: :numSentences,
142
+ highlights_per_url: :highlightsPerUrl,
143
+ query: :query
144
+ }
145
+ end
121
146
  end
122
147
  end
123
148
  end
@@ -5,7 +5,7 @@ require_relative "parameter_converter"
5
5
  module Exa
6
6
  module Services
7
7
  class Search
8
- VALID_SEARCH_TYPES = ["fast", "deep", "keyword", "auto"].freeze
8
+ VALID_SEARCH_TYPES = ["auto", "neural", "fast", "deep", "deep-reasoning", "instant"].freeze
9
9
  DEFAULT_SEARCH_TYPE = "auto"
10
10
 
11
11
  def initialize(connection, **params)
@@ -24,7 +24,8 @@ module Exa
24
24
  resolved_search_type: body["resolvedSearchType"],
25
25
  search_type: body["searchType"],
26
26
  context: body["context"],
27
- cost_dollars: body["costDollars"]
27
+ cost_dollars: body["costDollars"],
28
+ output: body["output"]
28
29
  )
29
30
  end
30
31
 
@@ -19,6 +19,7 @@ module Exa
19
19
  validate_enrichments!(params[:enrichments]) if params[:enrichments]
20
20
  validate_exclude!(params[:exclude]) if params[:exclude]
21
21
  validate_external_id!(params[:externalId]) if params[:externalId]
22
+ validate_title!(params[:title]) if params[:title]
22
23
  validate_metadata!(params[:metadata]) if params[:metadata]
23
24
  validate_no_duplicate_ids_in_import_and_scope!(params)
24
25
  end
@@ -158,6 +159,12 @@ module Exa
158
159
  raise ArgumentError, "externalId cannot exceed 300 characters" if external_id.length > 300
159
160
  end
160
161
 
162
+ def validate_title!(title)
163
+ raise ArgumentError, "title must be a String" unless title.is_a?(String)
164
+ raise ArgumentError, "title cannot be empty" if title.strip.empty?
165
+ raise ArgumentError, "title cannot exceed 1000 characters" if title.length > 1000
166
+ end
167
+
161
168
  def validate_metadata!(metadata)
162
169
  raise ArgumentError, "metadata must be a Hash" unless metadata.is_a?(Hash)
163
170
 
data/lib/exa/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Exa
4
- VERSION = "0.8.0"
4
+ VERSION = "0.10.0"
5
5
  end
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.8.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin Jackson