Skip to content

Commit 6eac9d3

Browse files
feat(cli): Add WebsetSearchFormatter and entity description support
Add dedicated formatter for webset searches and support custom entity descriptions in import-create. - Create WebsetSearchFormatter for webset-specific formatting - Update webset-search-create and webset-search-get to use new formatter - Add --entity-description flag to import-create for custom entity types - Add comprehensive tests for both features 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 661b546 commit 6eac9d3

7 files changed

Lines changed: 537 additions & 3 deletions

File tree

exe/exa-ai-import-create

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def parse_args(argv)
5656
--entity-type TYPE Entity type (options: #{VALID_ENTITY_TYPES.join(', ')})
5757
5858
Options:
59+
--entity-description TXT Description for custom entity type (required with --entity-type custom)
5960
--csv-identifier N CSV column identifier (0-indexed)
6061
--metadata JSON Custom metadata (supports @file.json)
6162
--quiet Suppress normal output (only show errors)
@@ -102,6 +103,9 @@ def parse_args(argv)
102103
when "--entity-type"
103104
args[:entity_type] = argv[i + 1]
104105
i += 2
106+
when "--entity-description"
107+
args[:entity_description] = argv[i + 1]
108+
i += 2
105109
when "--csv-identifier"
106110
args[:csv_identifier] = argv[i + 1].to_i
107111
i += 2
@@ -161,6 +165,17 @@ begin
161165
exit 1
162166
end
163167

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+
164179
# Validate file exists
165180
unless File.exist?(args[:file_path])
166181
$stderr.puts "Error: File not found: #{args[:file_path]}"
@@ -177,12 +192,15 @@ begin
177192
client = Exa::CLI::Base.build_client(api_key)
178193

179194
# Prepare import parameters
195+
entity = { type: args[:entity_type] }
196+
entity[:description] = args[:entity_description] if args[:entity_description]
197+
180198
import_params = {
181199
file_path: args[:file_path],
182200
count: args[:count],
183201
title: args[:title],
184202
format: args[:format],
185-
entity: { type: args[:entity_type] }
203+
entity: entity
186204
}
187205
import_params[:metadata] = args[:metadata] if args[:metadata]
188206

exe/exa-ai-webset-search-create

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ begin
203203
search = client.create_webset_search(webset_id: args[:webset_id], **search_params)
204204

205205
# Format and output result
206-
output = Exa::CLI::Formatters::SearchFormatter.format(search, output_format)
206+
output = Exa::CLI::Formatters::WebsetSearchFormatter.format(search, output_format)
207207
puts output
208208
$stdout.flush
209209

exe/exa-ai-webset-search-get

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ begin
7878
search = client.get_webset_search(webset_id: webset_id, id: search_id)
7979

8080
# Format and output
81-
output = Exa::CLI::Formatters::SearchFormatter.format(search, output_format)
81+
output = Exa::CLI::Formatters::WebsetSearchFormatter.format(search, output_format)
8282
puts output
8383
$stdout.flush
8484

lib/exa.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
require_relative "exa/cli/polling"
6868
require_relative "exa/cli/error_handler"
6969
require_relative "exa/cli/formatters/search_formatter"
70+
require_relative "exa/cli/formatters/webset_search_formatter"
7071
require_relative "exa/cli/formatters/context_formatter"
7172
require_relative "exa/cli/formatters/contents_formatter"
7273
require_relative "exa/cli/formatters/research_formatter"
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
class Exa::CLI::Formatters::WebsetSearchFormatterTest < Minitest::Test
6+
def test_json_format_returns_json_string
7+
search = create_webset_search
8+
output = Exa::CLI::Formatters::WebsetSearchFormatter.format(search, "json")
9+
10+
# Verify it's valid JSON
11+
parsed = JSON.parse(output)
12+
assert_equal "ws_search_123", parsed["id"]
13+
assert_equal "running", parsed["status"]
14+
assert_equal "AI startups", parsed["query"]
15+
end
16+
17+
def test_pretty_format_shows_search_metadata
18+
search = create_webset_search
19+
output = Exa::CLI::Formatters::WebsetSearchFormatter.format(search, "pretty")
20+
21+
# Verify it includes expected metadata
22+
assert_includes output, "Search ID: ws_search_123"
23+
assert_includes output, "Status: running"
24+
assert_includes output, "Query: AI startups"
25+
assert_includes output, "Behavior: override"
26+
assert_includes output, "Created: 2024-01-01T12:00:00Z"
27+
end
28+
29+
def test_pretty_format_excludes_results_property
30+
search = create_webset_search
31+
output = Exa::CLI::Formatters::WebsetSearchFormatter.format(search, "pretty")
32+
33+
# Verify .results is not accessed
34+
refute_includes output, "results"
35+
end
36+
37+
def test_text_format_shows_key_fields
38+
search = create_webset_search
39+
output = Exa::CLI::Formatters::WebsetSearchFormatter.format(search, "text")
40+
41+
# Verify it includes key fields
42+
assert_includes output, "ID: ws_search_123"
43+
assert_includes output, "Status: running"
44+
assert_includes output, "Query: AI startups"
45+
assert_includes output, "Behavior: override"
46+
end
47+
48+
def test_default_format_is_json
49+
search = create_webset_search
50+
output = Exa::CLI::Formatters::WebsetSearchFormatter.format(search, nil)
51+
52+
# Should default to JSON
53+
parsed = JSON.parse(output)
54+
assert_equal "ws_search_123", parsed["id"]
55+
end
56+
57+
def test_toon_format_returns_toon_string
58+
search = create_webset_search
59+
output = Exa::CLI::Formatters::WebsetSearchFormatter.format(search, "toon")
60+
61+
assert_instance_of String, output
62+
assert_includes output, "ws_search_123"
63+
assert_includes output, "AI startups"
64+
65+
# TOON should be more compact than JSON
66+
json_output = Exa::CLI::Formatters::WebsetSearchFormatter.format(search, "json")
67+
assert output.length < json_output.length
68+
end
69+
70+
def test_pretty_format_with_canceled_search
71+
search = create_canceled_webset_search
72+
output = Exa::CLI::Formatters::WebsetSearchFormatter.format(search, "pretty")
73+
74+
assert_includes output, "Status: canceled"
75+
assert_includes output, "Canceled: 2024-01-01T13:00:00Z"
76+
assert_includes output, "Cancel Reason: User requested cancellation"
77+
end
78+
79+
def test_pretty_format_with_custom_entity
80+
search = create_webset_search_with_entity
81+
output = Exa::CLI::Formatters::WebsetSearchFormatter.format(search, "pretty")
82+
83+
assert_includes output, "Entity Type: custom"
84+
end
85+
86+
def test_pretty_format_handles_nil_progress
87+
search = create_webset_search_without_progress
88+
# Ensure progress is nil
89+
assert_nil search.progress
90+
91+
output = Exa::CLI::Formatters::WebsetSearchFormatter.format(search, "pretty")
92+
# Should not fail and should have valid output
93+
assert_includes output, "Query:"
94+
end
95+
96+
private
97+
98+
def create_webset_search
99+
Exa::Resources::WebsetSearch.new(
100+
id: "ws_search_123",
101+
object: "webset.search",
102+
status: "running",
103+
webset_id: "ws_456",
104+
query: "AI startups",
105+
entity: { "type" => "company" },
106+
criteria: nil,
107+
count: 50,
108+
behavior: "override",
109+
exclude: nil,
110+
scope: nil,
111+
progress: 25,
112+
recall: false,
113+
metadata: nil,
114+
canceled_at: nil,
115+
canceled_reason: nil,
116+
created_at: "2024-01-01T12:00:00Z",
117+
updated_at: "2024-01-01T12:30:00Z"
118+
)
119+
end
120+
121+
def create_canceled_webset_search
122+
Exa::Resources::WebsetSearch.new(
123+
id: "ws_search_789",
124+
object: "webset.search",
125+
status: "canceled",
126+
webset_id: "ws_456",
127+
query: "tech companies",
128+
entity: nil,
129+
criteria: nil,
130+
count: nil,
131+
behavior: "override",
132+
exclude: nil,
133+
scope: nil,
134+
progress: 50,
135+
recall: false,
136+
metadata: nil,
137+
canceled_at: "2024-01-01T13:00:00Z",
138+
canceled_reason: "User requested cancellation",
139+
created_at: "2024-01-01T12:00:00Z",
140+
updated_at: "2024-01-01T13:00:00Z"
141+
)
142+
end
143+
144+
def create_webset_search_with_entity
145+
Exa::Resources::WebsetSearch.new(
146+
id: "ws_search_custom",
147+
object: "webset.search",
148+
status: "completed",
149+
webset_id: "ws_456",
150+
query: "vintage cars",
151+
entity: { "type" => "custom", "description" => "vintage cars" },
152+
criteria: nil,
153+
count: 20,
154+
behavior: "append",
155+
exclude: nil,
156+
scope: nil,
157+
progress: 100,
158+
recall: false,
159+
metadata: nil,
160+
canceled_at: nil,
161+
canceled_reason: nil,
162+
created_at: "2024-01-01T12:00:00Z",
163+
updated_at: "2024-01-01T12:45:00Z"
164+
)
165+
end
166+
167+
def create_webset_search_without_progress
168+
Exa::Resources::WebsetSearch.new(
169+
id: "ws_search_no_progress",
170+
object: "webset.search",
171+
status: "created",
172+
webset_id: "ws_456",
173+
query: "test query",
174+
entity: nil,
175+
criteria: nil,
176+
count: nil,
177+
behavior: "override",
178+
exclude: nil,
179+
scope: nil,
180+
progress: nil,
181+
recall: false,
182+
metadata: nil,
183+
canceled_at: nil,
184+
canceled_reason: nil,
185+
created_at: "2024-01-01T12:00:00Z",
186+
updated_at: "2024-01-01T12:00:00Z"
187+
)
188+
end
189+
end

0 commit comments

Comments
 (0)