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 +4 -4
- data/exe/exa-ai-search +40 -4
- data/exe/exa-ai-webset-create +5 -0
- data/exe/exa-ai-webset-update +11 -8
- data/lib/exa/cli/formatters/webset_formatter.rb +2 -0
- data/lib/exa/cli/search_parser.rb +49 -1
- data/lib/exa/client.rb +2 -1
- data/lib/exa/resources/search_result.rb +2 -1
- data/lib/exa/services/parameter_converter.rb +27 -2
- data/lib/exa/services/search.rb +3 -2
- data/lib/exa/services/websets/create_validator.rb +7 -0
- data/lib/exa/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ffa1108ddff0ff951dc18d12eeef57f74020a4c0425916a394fac95e4699fef5
|
|
4
|
+
data.tar.gz: 43c40cc61a7bee139dc3c179fe9d11c40b04dd506d53c22f88e312705ae9dfe3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,
|
|
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[:
|
|
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[:
|
|
137
|
-
search_params[:
|
|
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
|
|
data/exe/exa-ai-webset-create
CHANGED
|
@@ -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
|
data/exe/exa-ai-webset-update
CHANGED
|
@@ -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
|
-
|
|
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", "
|
|
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
|
data/lib/exa/services/search.rb
CHANGED
|
@@ -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", "
|
|
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