exa-ai 0.6.0 → 0.6.1
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-enrichment-create +0 -10
- data/exe/exa-ai-import-create +19 -1
- data/exe/exa-ai-webset-create +25 -6
- data/exe/exa-ai-webset-search-create +19 -6
- data/exe/exa-ai-webset-search-get +1 -1
- data/lib/exa/cli/formatters/enrichment_formatter.rb +54 -2
- data/lib/exa/cli/formatters/import_formatter.rb +70 -2
- data/lib/exa/cli/formatters/monitor_formatter.rb +65 -2
- data/lib/exa/cli/formatters/monitor_run_formatter.rb +53 -2
- data/lib/exa/cli/formatters/webset_formatter.rb +61 -1
- data/lib/exa/cli/formatters/webset_item_formatter.rb +50 -2
- data/lib/exa/cli/formatters/webset_search_formatter.rb +57 -0
- data/lib/exa/services/websets/create_validator.rb +15 -0
- data/lib/exa/version.rb +1 -1
- data/lib/exa.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4f55e9efe411da4b9eaf3018791aa7819fc8872c69e32369627014594eb23670
|
|
4
|
+
data.tar.gz: 0d074ce4bb6eaa2902b80fe5df239be717f182c3703ebfcd6c45944de7cd1245
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 732d915bf1eadcabff77ae2dcf7c573d426021bea3dac1fe0b2013958dde5a53129c62675ff66279b5204233c970b988ea72ad29ef94bb4b5219cba55910145b
|
|
7
|
+
data.tar.gz: f4f73347c282e2ebb2209afd42a02e1fb253e9d682a5959ce1cd2ea27519f132c156fb897bbd3177d24f4b88bd06874afd76b81bf70ccb225dcb3ad64ec6249b
|
|
@@ -51,9 +51,7 @@ def parse_args(argv)
|
|
|
51
51
|
--format TYPE One of: #{VALID_FORMATS.join(', ')}
|
|
52
52
|
|
|
53
53
|
Options:
|
|
54
|
-
--title TEXT Display title
|
|
55
54
|
--options JSON Array of {label: "..."} (required if format=options, supports @file.json)
|
|
56
|
-
--instructions TEXT Additional instructions
|
|
57
55
|
--metadata JSON Custom metadata (supports @file.json)
|
|
58
56
|
--wait Wait for enrichment to complete
|
|
59
57
|
--api-key KEY Exa API key (or set EXA_API_KEY env var)
|
|
@@ -91,15 +89,9 @@ def parse_args(argv)
|
|
|
91
89
|
when "--format"
|
|
92
90
|
args[:format] = argv[i + 1]
|
|
93
91
|
i += 2
|
|
94
|
-
when "--title"
|
|
95
|
-
args[:title] = argv[i + 1]
|
|
96
|
-
i += 2
|
|
97
92
|
when "--options"
|
|
98
93
|
args[:options] = parse_json_or_file(argv[i + 1])
|
|
99
94
|
i += 2
|
|
100
|
-
when "--instructions"
|
|
101
|
-
args[:instructions] = argv[i + 1]
|
|
102
|
-
i += 2
|
|
103
95
|
when "--metadata"
|
|
104
96
|
args[:metadata] = parse_json_or_file(argv[i + 1])
|
|
105
97
|
i += 2
|
|
@@ -170,9 +162,7 @@ begin
|
|
|
170
162
|
description: args[:description],
|
|
171
163
|
format: args[:format]
|
|
172
164
|
}
|
|
173
|
-
enrichment_params[:title] = args[:title] if args[:title]
|
|
174
165
|
enrichment_params[:options] = args[:options] if args[:options]
|
|
175
|
-
enrichment_params[:instructions] = args[:instructions] if args[:instructions]
|
|
176
166
|
enrichment_params[:metadata] = args[:metadata] if args[:metadata]
|
|
177
167
|
|
|
178
168
|
# Create enrichment
|
data/exe/exa-ai-import-create
CHANGED
|
@@ -56,6 +56,7 @@ def parse_args(argv)
|
|
|
56
56
|
--entity-type TYPE Entity type (options: #{VALID_ENTITY_TYPES.join(', ')})
|
|
57
57
|
|
|
58
58
|
Options:
|
|
59
|
+
--entity-description TXT Description for custom entity type (required with --entity-type custom)
|
|
59
60
|
--csv-identifier N CSV column identifier (0-indexed)
|
|
60
61
|
--metadata JSON Custom metadata (supports @file.json)
|
|
61
62
|
--quiet Suppress normal output (only show errors)
|
|
@@ -102,6 +103,9 @@ def parse_args(argv)
|
|
|
102
103
|
when "--entity-type"
|
|
103
104
|
args[:entity_type] = argv[i + 1]
|
|
104
105
|
i += 2
|
|
106
|
+
when "--entity-description"
|
|
107
|
+
args[:entity_description] = argv[i + 1]
|
|
108
|
+
i += 2
|
|
105
109
|
when "--csv-identifier"
|
|
106
110
|
args[:csv_identifier] = argv[i + 1].to_i
|
|
107
111
|
i += 2
|
|
@@ -161,6 +165,17 @@ begin
|
|
|
161
165
|
exit 1
|
|
162
166
|
end
|
|
163
167
|
|
|
168
|
+
# Validate entity-description for custom entity type
|
|
169
|
+
if args[:entity_type] == "custom"
|
|
170
|
+
unless args[:entity_description]
|
|
171
|
+
$stderr.puts "Error: --entity-description is required when --entity-type is 'custom'"
|
|
172
|
+
$stderr.puts "Run 'exa-ai import-create --help' for usage information"
|
|
173
|
+
exit 1
|
|
174
|
+
end
|
|
175
|
+
elsif args[:entity_description]
|
|
176
|
+
$stderr.puts "Warning: --entity-description is only used with --entity-type custom (ignoring)"
|
|
177
|
+
end
|
|
178
|
+
|
|
164
179
|
# Validate file exists
|
|
165
180
|
unless File.exist?(args[:file_path])
|
|
166
181
|
$stderr.puts "Error: File not found: #{args[:file_path]}"
|
|
@@ -177,12 +192,15 @@ begin
|
|
|
177
192
|
client = Exa::CLI::Base.build_client(api_key)
|
|
178
193
|
|
|
179
194
|
# Prepare import parameters
|
|
195
|
+
entity = { type: args[:entity_type] }
|
|
196
|
+
entity[:description] = args[:entity_description] if args[:entity_description]
|
|
197
|
+
|
|
180
198
|
import_params = {
|
|
181
199
|
file_path: args[:file_path],
|
|
182
200
|
count: args[:count],
|
|
183
201
|
title: args[:title],
|
|
184
202
|
format: args[:format],
|
|
185
|
-
entity:
|
|
203
|
+
entity: entity
|
|
186
204
|
}
|
|
187
205
|
import_params[:metadata] = args[:metadata] if args[:metadata]
|
|
188
206
|
|
data/exe/exa-ai-webset-create
CHANGED
|
@@ -80,32 +80,51 @@ def parse_args(argv)
|
|
|
80
80
|
Create a new webset from search criteria or an import
|
|
81
81
|
|
|
82
82
|
Required (choose one):
|
|
83
|
-
--search JSON Search configuration (supports @file.json)
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
--search JSON Search configuration as JSON (supports @file.json)
|
|
84
|
+
Format: {"query":"...","count":10,"scope":[...]}
|
|
85
|
+
The 'scope' field limits search to specific sources
|
|
86
|
+
--import ID Import/webset ID to attach data to this webset
|
|
87
|
+
(loads data but does NOT filter searches)
|
|
88
|
+
Format: import_abc123 or webset_xyz789
|
|
86
89
|
|
|
87
90
|
Options:
|
|
88
91
|
--enrichments JSON Array of enrichment configs (supports @file.json)
|
|
89
|
-
|
|
92
|
+
Format: [{"description":"...","format":"text"}]
|
|
93
|
+
--exclude JSON Sources to exclude from searches (supports @file.json)
|
|
94
|
+
Format: [{"source":"import|webset","id":"..."}]
|
|
90
95
|
--external-id ID External identifier for the webset
|
|
91
96
|
--metadata JSON Custom metadata (supports @file.json)
|
|
97
|
+
Format: {"key":"value"}
|
|
92
98
|
--wait Wait for webset to reach idle status
|
|
93
99
|
--api-key KEY Exa API key (or set EXA_API_KEY env var)
|
|
94
100
|
--output-format FMT Output format: json, pretty, or text (default: json)
|
|
95
101
|
--help, -h Show this help message
|
|
96
102
|
|
|
103
|
+
JSON Format Details:
|
|
104
|
+
search.scope Array of source references to limit search
|
|
105
|
+
Format: [{"source":"import|webset","id":"..."}]
|
|
106
|
+
With relationship (hop search):
|
|
107
|
+
[{"source":"webset","id":"ws_123",
|
|
108
|
+
"relationship":{"definition":"investors of","limit":3}}]
|
|
109
|
+
|
|
110
|
+
IMPORTANT: Cannot use the same import ID in both --import and search.scope
|
|
111
|
+
(this will return a 400 error from the API)
|
|
112
|
+
|
|
97
113
|
Examples:
|
|
98
114
|
# Create webset from search
|
|
99
115
|
exa-ai webset-create --search '{"query":"AI startups","count":10}'
|
|
100
116
|
exa-ai webset-create --search @search.json --enrichments @enrichments.json
|
|
101
117
|
exa-ai webset-create --search @search.json --wait
|
|
102
118
|
|
|
119
|
+
# Create webset with scoped search (filter to specific import)
|
|
120
|
+
exa-ai webset-create --search '{"query":"CEOs","count":10,"scope":[{"source":"import","id":"import_abc"}]}'
|
|
121
|
+
|
|
103
122
|
# Create webset from import
|
|
104
123
|
exa-ai webset-create --import import_abc123
|
|
105
124
|
exa-ai webset-create --import import_def456 --enrichments @enrichments.json
|
|
106
125
|
|
|
107
|
-
#
|
|
108
|
-
exa-ai webset-create --import
|
|
126
|
+
# Load import AND run search (search not scoped to import)
|
|
127
|
+
exa-ai webset-create --import import_abc123 --search '{"query":"investors","count":20}'
|
|
109
128
|
HELP
|
|
110
129
|
exit 0
|
|
111
130
|
else
|
|
@@ -104,11 +104,20 @@ def parse_args(argv)
|
|
|
104
104
|
--entity TYPE Entity type: person, company, article, research_paper, custom
|
|
105
105
|
--entity-description TXT Description for custom entity type (required with --entity custom)
|
|
106
106
|
--criteria JSON Search criteria array (supports @file.json)
|
|
107
|
+
Format: [{"description":"criterion 1"},{"description":"criterion 2"}]
|
|
107
108
|
--exclude JSON Items to exclude from results (supports @file.json)
|
|
109
|
+
Format: [{"source":"import|webset","id":"..."}]
|
|
108
110
|
--scope JSON Limit search to specific sources (supports @file.json)
|
|
111
|
+
Format: [{"source":"import|webset","id":"..."}]
|
|
112
|
+
Filters this search to only items from these sources
|
|
113
|
+
With relationship (hop search):
|
|
114
|
+
[{"source":"webset","id":"ws_123",
|
|
115
|
+
"relationship":{"definition":"investors of","limit":3}}]
|
|
109
116
|
--recall Estimate total available results
|
|
110
|
-
--behavior TYPE "override" or "append" (
|
|
117
|
+
--behavior TYPE "override" (replace items) or "append" (add items)
|
|
118
|
+
Default: override when scope is present, append otherwise
|
|
111
119
|
--metadata JSON Custom metadata (supports @file.json)
|
|
120
|
+
Format: {"key":"value"}
|
|
112
121
|
--api-key KEY Exa API key (or set EXA_API_KEY env var)
|
|
113
122
|
--output-format FMT Output format: json, pretty, or text (default: json)
|
|
114
123
|
--help, -h Show this help message
|
|
@@ -121,11 +130,15 @@ def parse_args(argv)
|
|
|
121
130
|
exa-ai webset-search-create ws_123 --query "tech CEOs" --entity person
|
|
122
131
|
exa-ai webset-search-create ws_123 --query "Silicon Valley firms" --entity company
|
|
123
132
|
|
|
124
|
-
#
|
|
125
|
-
exa-ai webset-search-create ws_123 --query "
|
|
126
|
-
--
|
|
133
|
+
# Scoped search (filter to specific import)
|
|
134
|
+
exa-ai webset-search-create ws_123 --query "CTOs" \\
|
|
135
|
+
--scope '[{"source":"import","id":"import_abc"}]'
|
|
127
136
|
|
|
128
|
-
#
|
|
137
|
+
# Hop search (find investors of companies in webset)
|
|
138
|
+
exa-ai webset-search-create ws_123 --query "investors" \\
|
|
139
|
+
--scope '[{"source":"webset","id":"ws_companies","relationship":{"definition":"investors of","limit":5}}]'
|
|
140
|
+
|
|
141
|
+
# Search with criteria and behavior
|
|
129
142
|
exa-ai webset-search-create ws_123 --query "machine learning" --count 50
|
|
130
143
|
exa-ai webset-search-create ws_123 --query "research" --behavior append --recall
|
|
131
144
|
HELP
|
|
@@ -203,7 +216,7 @@ begin
|
|
|
203
216
|
search = client.create_webset_search(webset_id: args[:webset_id], **search_params)
|
|
204
217
|
|
|
205
218
|
# Format and output result
|
|
206
|
-
output = Exa::CLI::Formatters::
|
|
219
|
+
output = Exa::CLI::Formatters::WebsetSearchFormatter.format(search, output_format)
|
|
207
220
|
puts output
|
|
208
221
|
$stdout.flush
|
|
209
222
|
|
|
@@ -78,7 +78,7 @@ begin
|
|
|
78
78
|
search = client.get_webset_search(webset_id: webset_id, id: search_id)
|
|
79
79
|
|
|
80
80
|
# Format and output
|
|
81
|
-
output = Exa::CLI::Formatters::
|
|
81
|
+
output = Exa::CLI::Formatters::WebsetSearchFormatter.format(search, output_format)
|
|
82
82
|
puts output
|
|
83
83
|
$stdout.flush
|
|
84
84
|
|
|
@@ -9,7 +9,7 @@ module Exa
|
|
|
9
9
|
when "json"
|
|
10
10
|
JSON.generate(enrichment.to_h)
|
|
11
11
|
when "pretty"
|
|
12
|
-
|
|
12
|
+
format_as_pretty(enrichment)
|
|
13
13
|
when "text"
|
|
14
14
|
format_as_text(enrichment)
|
|
15
15
|
when "toon"
|
|
@@ -24,7 +24,7 @@ module Exa
|
|
|
24
24
|
when "json"
|
|
25
25
|
JSON.generate(collection.to_h)
|
|
26
26
|
when "pretty"
|
|
27
|
-
|
|
27
|
+
format_collection_as_pretty(collection)
|
|
28
28
|
when "text"
|
|
29
29
|
format_collection_as_text(collection)
|
|
30
30
|
when "toon"
|
|
@@ -34,6 +34,31 @@ module Exa
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def self.format_as_pretty(enrichment)
|
|
38
|
+
lines = []
|
|
39
|
+
lines << "Enrichment ID: #{enrichment.id}"
|
|
40
|
+
lines << "Webset ID: #{enrichment.webset_id}" if enrichment.webset_id
|
|
41
|
+
lines << "Status: #{enrichment.status}"
|
|
42
|
+
lines << "Title: #{enrichment.title}" if enrichment.title
|
|
43
|
+
lines << "Description: #{enrichment.description}" if enrichment.description
|
|
44
|
+
lines << "Format: #{enrichment.format}" if enrichment.format
|
|
45
|
+
|
|
46
|
+
if enrichment.options && !enrichment.options.empty?
|
|
47
|
+
lines << ""
|
|
48
|
+
lines << "Options (#{enrichment.options.length}):"
|
|
49
|
+
enrichment.options.each do |option|
|
|
50
|
+
lines << " • #{option['label']}" if option['label']
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
lines << ""
|
|
55
|
+
lines << "Created: #{enrichment.created_at}" if enrichment.created_at
|
|
56
|
+
lines << "Updated: #{enrichment.updated_at}" if enrichment.updated_at
|
|
57
|
+
|
|
58
|
+
lines.join("\n")
|
|
59
|
+
end
|
|
60
|
+
private_class_method :format_as_pretty
|
|
61
|
+
|
|
37
62
|
def self.format_as_text(enrichment)
|
|
38
63
|
lines = []
|
|
39
64
|
lines << "Enrichment: #{enrichment.id}"
|
|
@@ -57,6 +82,33 @@ module Exa
|
|
|
57
82
|
end
|
|
58
83
|
private_class_method :format_as_text
|
|
59
84
|
|
|
85
|
+
def self.format_collection_as_pretty(collection)
|
|
86
|
+
lines = []
|
|
87
|
+
lines << "Enrichments (#{collection.data.length} items)"
|
|
88
|
+
lines << ""
|
|
89
|
+
|
|
90
|
+
collection.data.each_with_index do |enr, idx|
|
|
91
|
+
lines << "" if idx > 0 # Blank line between enrichments
|
|
92
|
+
|
|
93
|
+
lines << "Enrichment ID: #{enr['id']}"
|
|
94
|
+
lines << "Webset ID: #{enr['websetId']}" if enr['websetId']
|
|
95
|
+
lines << "Status: #{enr['status']}"
|
|
96
|
+
lines << "Title: #{enr['title']}" if enr['title']
|
|
97
|
+
lines << "Description: #{enr['description']}" if enr['description']
|
|
98
|
+
lines << "Format: #{enr['format']}" if enr['format']
|
|
99
|
+
lines << "Created: #{enr['createdAt']}" if enr['createdAt']
|
|
100
|
+
lines << "Updated: #{enr['updatedAt']}" if enr['updatedAt']
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if collection.has_more
|
|
104
|
+
lines << ""
|
|
105
|
+
lines << "Next Cursor: #{collection.next_cursor}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
lines.join("\n")
|
|
109
|
+
end
|
|
110
|
+
private_class_method :format_collection_as_pretty
|
|
111
|
+
|
|
60
112
|
def self.format_collection_as_text(collection)
|
|
61
113
|
lines = ["Enrichments (#{collection.data.length} items):"]
|
|
62
114
|
collection.data.each do |enr|
|
|
@@ -9,7 +9,7 @@ module Exa
|
|
|
9
9
|
when "json"
|
|
10
10
|
JSON.generate(import.to_h)
|
|
11
11
|
when "pretty"
|
|
12
|
-
|
|
12
|
+
format_as_pretty(import)
|
|
13
13
|
when "text"
|
|
14
14
|
format_as_text(import)
|
|
15
15
|
when "toon"
|
|
@@ -24,7 +24,7 @@ module Exa
|
|
|
24
24
|
when "json"
|
|
25
25
|
JSON.generate(collection.to_h)
|
|
26
26
|
when "pretty"
|
|
27
|
-
|
|
27
|
+
format_collection_as_pretty(collection)
|
|
28
28
|
when "text"
|
|
29
29
|
format_collection_as_text(collection)
|
|
30
30
|
when "toon"
|
|
@@ -34,6 +34,43 @@ module Exa
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def self.format_as_pretty(import)
|
|
38
|
+
lines = []
|
|
39
|
+
lines << "Import ID: #{import.id}"
|
|
40
|
+
lines << "Status: #{import.status}"
|
|
41
|
+
lines << "Title: #{import.title}" if import.title
|
|
42
|
+
lines << "Format: #{import.format}" if import.format
|
|
43
|
+
|
|
44
|
+
if import.entity
|
|
45
|
+
entity_type = import.entity['type'] || import.entity[:type]
|
|
46
|
+
lines << "Entity Type: #{entity_type}" if entity_type
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
lines << "Count: #{import.count}" if import.count
|
|
50
|
+
|
|
51
|
+
if import.failed?
|
|
52
|
+
lines << ""
|
|
53
|
+
lines << "Failure:"
|
|
54
|
+
lines << " Reason: #{import.failed_reason}" if import.failed_reason
|
|
55
|
+
lines << " Message: #{import.failed_message}" if import.failed_message
|
|
56
|
+
lines << " Failed At: #{import.failed_at}" if import.failed_at
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if import.upload_url
|
|
60
|
+
lines << ""
|
|
61
|
+
lines << "Upload:"
|
|
62
|
+
lines << " URL: #{import.upload_url}"
|
|
63
|
+
lines << " Valid Until: #{import.upload_valid_until}" if import.upload_valid_until
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
lines << ""
|
|
67
|
+
lines << "Created: #{import.created_at}" if import.created_at
|
|
68
|
+
lines << "Updated: #{import.updated_at}" if import.updated_at
|
|
69
|
+
|
|
70
|
+
lines.join("\n")
|
|
71
|
+
end
|
|
72
|
+
private_class_method :format_as_pretty
|
|
73
|
+
|
|
37
74
|
def self.format_as_text(import)
|
|
38
75
|
lines = []
|
|
39
76
|
lines << "Import: #{import.id}"
|
|
@@ -68,6 +105,37 @@ module Exa
|
|
|
68
105
|
end
|
|
69
106
|
private_class_method :format_as_text
|
|
70
107
|
|
|
108
|
+
def self.format_collection_as_pretty(collection)
|
|
109
|
+
lines = []
|
|
110
|
+
lines << "Imports (#{collection.data.length} items)"
|
|
111
|
+
lines << ""
|
|
112
|
+
|
|
113
|
+
collection.data.each_with_index do |imp, idx|
|
|
114
|
+
lines << "" if idx > 0 # Blank line between imports
|
|
115
|
+
|
|
116
|
+
lines << "Import ID: #{imp.id}"
|
|
117
|
+
lines << "Status: #{imp.status}"
|
|
118
|
+
lines << "Title: #{imp.title}" if imp.title
|
|
119
|
+
lines << "Format: #{imp.format}" if imp.format
|
|
120
|
+
lines << "Entity Type: #{imp.entity['type']}" if imp.entity && imp.entity['type']
|
|
121
|
+
lines << "Count: #{imp.count}" if imp.count
|
|
122
|
+
lines << "Created: #{imp.created_at}" if imp.created_at
|
|
123
|
+
lines << "Updated: #{imp.updated_at}" if imp.updated_at
|
|
124
|
+
|
|
125
|
+
if imp.status == 'failed'
|
|
126
|
+
lines << "Failed Reason: #{imp.failed_reason}" if imp.failed_reason
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
if collection.has_more
|
|
131
|
+
lines << ""
|
|
132
|
+
lines << "Next Cursor: #{collection.next_cursor}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
lines.join("\n")
|
|
136
|
+
end
|
|
137
|
+
private_class_method :format_collection_as_pretty
|
|
138
|
+
|
|
71
139
|
def self.format_collection_as_text(collection)
|
|
72
140
|
lines = ["Imports (#{collection.data.length} items):"]
|
|
73
141
|
collection.data.each do |imp|
|
|
@@ -9,7 +9,7 @@ module Exa
|
|
|
9
9
|
when "json"
|
|
10
10
|
JSON.generate(monitor.to_h)
|
|
11
11
|
when "pretty"
|
|
12
|
-
|
|
12
|
+
format_as_pretty(monitor)
|
|
13
13
|
when "text"
|
|
14
14
|
format_as_text(monitor)
|
|
15
15
|
when "toon"
|
|
@@ -24,7 +24,7 @@ module Exa
|
|
|
24
24
|
when "json"
|
|
25
25
|
JSON.generate(collection.to_h)
|
|
26
26
|
when "pretty"
|
|
27
|
-
|
|
27
|
+
format_collection_as_pretty(collection)
|
|
28
28
|
when "text"
|
|
29
29
|
format_collection_as_text(collection)
|
|
30
30
|
when "toon"
|
|
@@ -34,6 +34,35 @@ module Exa
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def self.format_as_pretty(monitor)
|
|
38
|
+
lines = []
|
|
39
|
+
lines << "Monitor ID: #{monitor.id}"
|
|
40
|
+
lines << "Webset ID: #{monitor.webset_id}" if monitor.webset_id
|
|
41
|
+
lines << "Status: #{monitor.status}"
|
|
42
|
+
|
|
43
|
+
if monitor.cadence
|
|
44
|
+
lines << ""
|
|
45
|
+
lines << "Cadence:"
|
|
46
|
+
lines << " Cron: #{monitor.cadence['cron']}" if monitor.cadence['cron']
|
|
47
|
+
lines << " Timezone: #{monitor.cadence['timezone']}" if monitor.cadence['timezone']
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if monitor.behavior
|
|
51
|
+
lines << ""
|
|
52
|
+
lines << "Behavior:"
|
|
53
|
+
lines << " Type: #{monitor.behavior['type']}" if monitor.behavior['type']
|
|
54
|
+
lines << " Query: #{monitor.behavior['query']}" if monitor.behavior['query']
|
|
55
|
+
lines << " Count: #{monitor.behavior['count']}" if monitor.behavior['count']
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
lines << ""
|
|
59
|
+
lines << "Created: #{monitor.created_at}" if monitor.created_at
|
|
60
|
+
lines << "Updated: #{monitor.updated_at}" if monitor.updated_at
|
|
61
|
+
|
|
62
|
+
lines.join("\n")
|
|
63
|
+
end
|
|
64
|
+
private_class_method :format_as_pretty
|
|
65
|
+
|
|
37
66
|
def self.format_as_text(monitor)
|
|
38
67
|
lines = []
|
|
39
68
|
lines << "Monitor: #{monitor.id}"
|
|
@@ -60,6 +89,40 @@ module Exa
|
|
|
60
89
|
end
|
|
61
90
|
private_class_method :format_as_text
|
|
62
91
|
|
|
92
|
+
def self.format_collection_as_pretty(collection)
|
|
93
|
+
lines = []
|
|
94
|
+
lines << "Monitors (#{collection.data.length} items)"
|
|
95
|
+
lines << ""
|
|
96
|
+
|
|
97
|
+
collection.data.each_with_index do |mon, idx|
|
|
98
|
+
lines << "" if idx > 0 # Blank line between monitors
|
|
99
|
+
|
|
100
|
+
lines << "Monitor ID: #{mon['id']}"
|
|
101
|
+
lines << "Webset ID: #{mon['websetId']}" if mon['websetId']
|
|
102
|
+
lines << "Status: #{mon['status']}"
|
|
103
|
+
|
|
104
|
+
if mon['cadence']
|
|
105
|
+
lines << "Cron: #{mon['cadence']['cron']}" if mon['cadence']['cron']
|
|
106
|
+
lines << "Timezone: #{mon['cadence']['timezone']}" if mon['cadence']['timezone']
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if mon['behavior']
|
|
110
|
+
lines << "Query: #{mon['behavior']['query']}" if mon['behavior']['query']
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
lines << "Created: #{mon['createdAt']}" if mon['createdAt']
|
|
114
|
+
lines << "Updated: #{mon['updatedAt']}" if mon['updatedAt']
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if collection.has_more
|
|
118
|
+
lines << ""
|
|
119
|
+
lines << "Next Cursor: #{collection.next_cursor}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
lines.join("\n")
|
|
123
|
+
end
|
|
124
|
+
private_class_method :format_collection_as_pretty
|
|
125
|
+
|
|
63
126
|
def self.format_collection_as_text(collection)
|
|
64
127
|
lines = ["Monitors (#{collection.data.length} items):"]
|
|
65
128
|
collection.data.each do |mon|
|
|
@@ -9,7 +9,7 @@ module Exa
|
|
|
9
9
|
when "json"
|
|
10
10
|
JSON.generate(monitor_run.to_h)
|
|
11
11
|
when "pretty"
|
|
12
|
-
|
|
12
|
+
format_as_pretty(monitor_run)
|
|
13
13
|
when "text"
|
|
14
14
|
format_as_text(monitor_run)
|
|
15
15
|
when "toon"
|
|
@@ -24,7 +24,7 @@ module Exa
|
|
|
24
24
|
when "json"
|
|
25
25
|
JSON.generate(collection.to_h)
|
|
26
26
|
when "pretty"
|
|
27
|
-
|
|
27
|
+
format_collection_as_pretty(collection)
|
|
28
28
|
when "text"
|
|
29
29
|
format_collection_as_text(collection)
|
|
30
30
|
when "toon"
|
|
@@ -34,6 +34,27 @@ module Exa
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def self.format_as_pretty(monitor_run)
|
|
38
|
+
lines = []
|
|
39
|
+
lines << "Monitor Run ID: #{monitor_run.id}"
|
|
40
|
+
lines << "Monitor ID: #{monitor_run.monitor_id}" if monitor_run.monitor_id
|
|
41
|
+
lines << "Status: #{monitor_run.status}"
|
|
42
|
+
|
|
43
|
+
lines << ""
|
|
44
|
+
lines << "Created: #{monitor_run.created_at}" if monitor_run.created_at
|
|
45
|
+
lines << "Updated: #{monitor_run.updated_at}" if monitor_run.updated_at
|
|
46
|
+
lines << "Completed: #{monitor_run.completed_at}" if monitor_run.completed_at
|
|
47
|
+
|
|
48
|
+
if monitor_run.failed?
|
|
49
|
+
lines << ""
|
|
50
|
+
lines << "Failed: #{monitor_run.failed_at}" if monitor_run.failed_at
|
|
51
|
+
lines << "Reason: #{monitor_run.failed_reason}" if monitor_run.failed_reason
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
lines.join("\n")
|
|
55
|
+
end
|
|
56
|
+
private_class_method :format_as_pretty
|
|
57
|
+
|
|
37
58
|
def self.format_as_text(monitor_run)
|
|
38
59
|
lines = []
|
|
39
60
|
lines << "Monitor Run: #{monitor_run.id}"
|
|
@@ -53,6 +74,36 @@ module Exa
|
|
|
53
74
|
end
|
|
54
75
|
private_class_method :format_as_text
|
|
55
76
|
|
|
77
|
+
def self.format_collection_as_pretty(collection)
|
|
78
|
+
lines = []
|
|
79
|
+
lines << "Monitor Runs (#{collection.data.length} items)"
|
|
80
|
+
lines << ""
|
|
81
|
+
|
|
82
|
+
collection.data.each_with_index do |run, idx|
|
|
83
|
+
lines << "" if idx > 0 # Blank line between runs
|
|
84
|
+
|
|
85
|
+
lines << "Monitor Run ID: #{run['id']}"
|
|
86
|
+
lines << "Monitor ID: #{run['monitorId']}" if run['monitorId']
|
|
87
|
+
lines << "Status: #{run['status']}"
|
|
88
|
+
lines << "Created: #{run['createdAt']}" if run['createdAt']
|
|
89
|
+
lines << "Updated: #{run['updatedAt']}" if run['updatedAt']
|
|
90
|
+
lines << "Completed: #{run['completedAt']}" if run['completedAt']
|
|
91
|
+
|
|
92
|
+
if run['status'] == 'failed'
|
|
93
|
+
lines << "Failed: #{run['failedAt']}" if run['failedAt']
|
|
94
|
+
lines << "Reason: #{run['failedReason']}" if run['failedReason']
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if collection.has_more
|
|
99
|
+
lines << ""
|
|
100
|
+
lines << "Next Cursor: #{collection.next_cursor}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
lines.join("\n")
|
|
104
|
+
end
|
|
105
|
+
private_class_method :format_collection_as_pretty
|
|
106
|
+
|
|
56
107
|
def self.format_collection_as_text(collection)
|
|
57
108
|
lines = ["Monitor Runs (#{collection.data.length} items):"]
|
|
58
109
|
collection.data.each do |run|
|
|
@@ -24,7 +24,7 @@ module Exa
|
|
|
24
24
|
when "json"
|
|
25
25
|
JSON.generate(collection.to_h)
|
|
26
26
|
when "pretty"
|
|
27
|
-
|
|
27
|
+
format_collection_as_pretty(collection)
|
|
28
28
|
when "text"
|
|
29
29
|
format_collection_as_text(collection)
|
|
30
30
|
when "toon"
|
|
@@ -66,6 +66,66 @@ module Exa
|
|
|
66
66
|
lines.join("\n")
|
|
67
67
|
end
|
|
68
68
|
private_class_method :format_collection_as_text
|
|
69
|
+
|
|
70
|
+
def self.format_collection_as_pretty(collection)
|
|
71
|
+
lines = []
|
|
72
|
+
|
|
73
|
+
# Header with count and pagination info
|
|
74
|
+
header = "Websets (#{collection.data.length} items)"
|
|
75
|
+
header += " - Page #{collection.has_more ? '1 of many' : '1 of 1'}" if collection.data.any?
|
|
76
|
+
lines << header
|
|
77
|
+
|
|
78
|
+
if collection.has_more
|
|
79
|
+
lines << "Next Cursor: #{collection.next_cursor}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
lines << ""
|
|
83
|
+
|
|
84
|
+
# Format each webset
|
|
85
|
+
collection.data.each_with_index do |ws, idx|
|
|
86
|
+
lines << "" if idx > 0 # Blank line between websets
|
|
87
|
+
|
|
88
|
+
lines << "Webset ID: #{ws['id']}"
|
|
89
|
+
lines << "Status: #{ws['status']}"
|
|
90
|
+
lines << "Title: #{ws['title']}" if ws['title']
|
|
91
|
+
lines << "External ID: #{ws['externalId']}" if ws['externalId']
|
|
92
|
+
lines << "Created: #{ws['createdAt']}" if ws['createdAt']
|
|
93
|
+
lines << "Updated: #{ws['updatedAt']}" if ws['updatedAt']
|
|
94
|
+
|
|
95
|
+
# Searches
|
|
96
|
+
if ws['searches'] && !ws['searches'].empty?
|
|
97
|
+
lines << ""
|
|
98
|
+
lines << "Searches (#{ws['searches'].length}):"
|
|
99
|
+
ws['searches'].each do |search|
|
|
100
|
+
status_indicator = case search['status']
|
|
101
|
+
when 'completed' then '✓'
|
|
102
|
+
when 'running' then '→'
|
|
103
|
+
when 'failed' then '✗'
|
|
104
|
+
else '•'
|
|
105
|
+
end
|
|
106
|
+
lines << " #{status_indicator} #{search['query']} (#{search['status']})" if search['query']
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Enrichments
|
|
111
|
+
if ws['enrichments'] && !ws['enrichments'].empty?
|
|
112
|
+
lines << "Enrichments: #{ws['enrichments'].length}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Monitors
|
|
116
|
+
if ws['monitors'] && !ws['monitors'].empty?
|
|
117
|
+
lines << "Monitors: #{ws['monitors'].length}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Imports
|
|
121
|
+
if ws['imports'] && !ws['imports'].empty?
|
|
122
|
+
lines << "Imports: #{ws['imports'].length}"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
lines.join("\n")
|
|
127
|
+
end
|
|
128
|
+
private_class_method :format_collection_as_pretty
|
|
69
129
|
end
|
|
70
130
|
end
|
|
71
131
|
end
|
|
@@ -9,7 +9,7 @@ module Exa
|
|
|
9
9
|
when "json"
|
|
10
10
|
JSON.generate(item)
|
|
11
11
|
when "pretty"
|
|
12
|
-
|
|
12
|
+
format_as_pretty(item)
|
|
13
13
|
when "text"
|
|
14
14
|
format_as_text(item)
|
|
15
15
|
when "toon"
|
|
@@ -24,7 +24,7 @@ module Exa
|
|
|
24
24
|
when "json"
|
|
25
25
|
JSON.generate(items)
|
|
26
26
|
when "pretty"
|
|
27
|
-
|
|
27
|
+
format_collection_as_pretty(items)
|
|
28
28
|
when "text"
|
|
29
29
|
format_collection_as_text(items)
|
|
30
30
|
when "toon"
|
|
@@ -34,6 +34,27 @@ module Exa
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def self.format_as_pretty(item)
|
|
38
|
+
lines = []
|
|
39
|
+
lines << "Item ID: #{item['id']}"
|
|
40
|
+
lines << "URL: #{item['url']}" if item['url']
|
|
41
|
+
lines << "Title: #{item['title']}" if item['title']
|
|
42
|
+
lines << "Status: #{item['status']}" if item['status']
|
|
43
|
+
lines << "Created: #{item['createdAt']}" if item['createdAt']
|
|
44
|
+
lines << "Updated: #{item['updatedAt']}" if item['updatedAt']
|
|
45
|
+
|
|
46
|
+
if item['entity']
|
|
47
|
+
lines << ""
|
|
48
|
+
lines << "Entity:"
|
|
49
|
+
lines << " Type: #{item['entity']['type']}" if item['entity']['type']
|
|
50
|
+
lines << " Name: #{item['entity']['name']}" if item['entity']['name']
|
|
51
|
+
lines << " Description: #{item['entity']['description']}" if item['entity']['description']
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
lines.join("\n")
|
|
55
|
+
end
|
|
56
|
+
private_class_method :format_as_pretty
|
|
57
|
+
|
|
37
58
|
def self.format_as_text(item)
|
|
38
59
|
lines = []
|
|
39
60
|
lines << "Item: #{item['id']}"
|
|
@@ -53,6 +74,33 @@ module Exa
|
|
|
53
74
|
end
|
|
54
75
|
private_class_method :format_as_text
|
|
55
76
|
|
|
77
|
+
def self.format_collection_as_pretty(items)
|
|
78
|
+
lines = []
|
|
79
|
+
lines << "Items (#{items.length})"
|
|
80
|
+
lines << ""
|
|
81
|
+
|
|
82
|
+
items.each_with_index do |item, idx|
|
|
83
|
+
lines << "" if idx > 0 # Blank line between items
|
|
84
|
+
|
|
85
|
+
lines << "Item ID: #{item['id']}"
|
|
86
|
+
lines << "URL: #{item['url']}" if item['url']
|
|
87
|
+
lines << "Title: #{item['title']}" if item['title']
|
|
88
|
+
lines << "Status: #{item['status']}" if item['status']
|
|
89
|
+
lines << "Created: #{item['createdAt']}" if item['createdAt']
|
|
90
|
+
lines << "Updated: #{item['updatedAt']}" if item['updatedAt']
|
|
91
|
+
|
|
92
|
+
if item['entity']
|
|
93
|
+
entity_name = item['entity']['name']
|
|
94
|
+
entity_type = item['entity']['type']
|
|
95
|
+
lines << "Entity: #{entity_name}" if entity_name
|
|
96
|
+
lines << "Entity Type: #{entity_type}" if entity_type && !entity_name
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
lines.join("\n")
|
|
101
|
+
end
|
|
102
|
+
private_class_method :format_collection_as_pretty
|
|
103
|
+
|
|
56
104
|
def self.format_collection_as_text(items)
|
|
57
105
|
lines = ["Items (#{items.length} total):"]
|
|
58
106
|
items.each_with_index do |item, idx|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Exa
|
|
4
|
+
module CLI
|
|
5
|
+
module Formatters
|
|
6
|
+
class WebsetSearchFormatter
|
|
7
|
+
def self.format(search, format)
|
|
8
|
+
case format
|
|
9
|
+
when "json"
|
|
10
|
+
JSON.pretty_generate(search.to_h)
|
|
11
|
+
when "pretty"
|
|
12
|
+
format_pretty(search)
|
|
13
|
+
when "text"
|
|
14
|
+
format_text(search)
|
|
15
|
+
when "toon"
|
|
16
|
+
Exa::CLI::Base.encode_as_toon(search.to_h)
|
|
17
|
+
else
|
|
18
|
+
JSON.pretty_generate(search.to_h)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def self.format_pretty(search)
|
|
25
|
+
output = []
|
|
26
|
+
output << "Search ID: #{search.id}"
|
|
27
|
+
output << "Status: #{search.status}"
|
|
28
|
+
output << "Query: #{search.query}"
|
|
29
|
+
output << "Entity Type: #{search.entity&.[]('type') || 'N/A'}" if search.entity
|
|
30
|
+
output << "Count: #{search.count}" if search.count
|
|
31
|
+
output << "Behavior: #{search.behavior}"
|
|
32
|
+
output << "Recall: #{search.recall}" if search.recall
|
|
33
|
+
output << "Created: #{search.created_at}"
|
|
34
|
+
output << "Updated: #{search.updated_at}"
|
|
35
|
+
output << "Progress: #{search.progress}" if search.progress
|
|
36
|
+
output << ""
|
|
37
|
+
|
|
38
|
+
if search.canceled?
|
|
39
|
+
output << "Canceled: #{search.canceled_at}"
|
|
40
|
+
output << "Cancel Reason: #{search.canceled_reason}" if search.canceled_reason
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
output.join("\n")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.format_text(search)
|
|
47
|
+
[
|
|
48
|
+
"ID: #{search.id}",
|
|
49
|
+
"Status: #{search.status}",
|
|
50
|
+
"Query: #{search.query}",
|
|
51
|
+
"Behavior: #{search.behavior}"
|
|
52
|
+
].join("\n")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -20,6 +20,7 @@ module Exa
|
|
|
20
20
|
validate_exclude!(params[:exclude]) if params[:exclude]
|
|
21
21
|
validate_external_id!(params[:externalId]) if params[:externalId]
|
|
22
22
|
validate_metadata!(params[:metadata]) if params[:metadata]
|
|
23
|
+
validate_no_duplicate_ids_in_import_and_scope!(params)
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
private
|
|
@@ -184,6 +185,20 @@ module Exa
|
|
|
184
185
|
raise ArgumentError, "#{name} must be at least #{min} characters" if min && value.length < min
|
|
185
186
|
raise ArgumentError, "#{name} cannot exceed #{max} characters" if max && value.length > max
|
|
186
187
|
end
|
|
188
|
+
|
|
189
|
+
def validate_no_duplicate_ids_in_import_and_scope!(params)
|
|
190
|
+
return unless params[:import] && params[:search] && params[:search][:scope]
|
|
191
|
+
|
|
192
|
+
import_ids = params[:import].map { |item| item[:id] }
|
|
193
|
+
scope_ids = params[:search][:scope].map { |item| item[:id] }
|
|
194
|
+
|
|
195
|
+
duplicates = import_ids & scope_ids
|
|
196
|
+
|
|
197
|
+
return if duplicates.empty?
|
|
198
|
+
|
|
199
|
+
raise ArgumentError,
|
|
200
|
+
"Cannot use the same import/webset ID in both :import and search[:scope]: #{duplicates.join(', ')}"
|
|
201
|
+
end
|
|
187
202
|
end
|
|
188
203
|
end
|
|
189
204
|
end
|
data/lib/exa/version.rb
CHANGED
data/lib/exa.rb
CHANGED
|
@@ -67,6 +67,7 @@ require_relative "exa/cli/base"
|
|
|
67
67
|
require_relative "exa/cli/polling"
|
|
68
68
|
require_relative "exa/cli/error_handler"
|
|
69
69
|
require_relative "exa/cli/formatters/search_formatter"
|
|
70
|
+
require_relative "exa/cli/formatters/webset_search_formatter"
|
|
70
71
|
require_relative "exa/cli/formatters/context_formatter"
|
|
71
72
|
require_relative "exa/cli/formatters/contents_formatter"
|
|
72
73
|
require_relative "exa/cli/formatters/research_formatter"
|
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.6.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Benjamin Jackson
|
|
@@ -206,6 +206,7 @@ files:
|
|
|
206
206
|
- lib/exa/cli/formatters/search_formatter.rb
|
|
207
207
|
- lib/exa/cli/formatters/webset_formatter.rb
|
|
208
208
|
- lib/exa/cli/formatters/webset_item_formatter.rb
|
|
209
|
+
- lib/exa/cli/formatters/webset_search_formatter.rb
|
|
209
210
|
- lib/exa/cli/polling.rb
|
|
210
211
|
- lib/exa/client.rb
|
|
211
212
|
- lib/exa/connection.rb
|