exa-ai 0.1.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 +21 -0
- data/README.md +562 -0
- data/exe/exa-ai +95 -0
- data/exe/exa-ai-answer +131 -0
- data/exe/exa-ai-context +104 -0
- data/exe/exa-ai-get-contents +114 -0
- data/exe/exa-ai-research-get +110 -0
- data/exe/exa-ai-research-list +95 -0
- data/exe/exa-ai-research-start +175 -0
- data/exe/exa-ai-search +134 -0
- data/lib/exa/cli/base.rb +51 -0
- data/lib/exa/cli/error_handler.rb +98 -0
- data/lib/exa/cli/formatters/answer_formatter.rb +63 -0
- data/lib/exa/cli/formatters/contents_formatter.rb +50 -0
- data/lib/exa/cli/formatters/context_formatter.rb +58 -0
- data/lib/exa/cli/formatters/research_formatter.rb +120 -0
- data/lib/exa/cli/formatters/search_formatter.rb +44 -0
- data/lib/exa/cli/polling.rb +46 -0
- data/lib/exa/client.rb +132 -0
- data/lib/exa/connection.rb +32 -0
- data/lib/exa/error.rb +31 -0
- data/lib/exa/middleware/raise_error.rb +55 -0
- data/lib/exa/resources/answer.rb +20 -0
- data/lib/exa/resources/contents_result.rb +29 -0
- data/lib/exa/resources/context_result.rb +37 -0
- data/lib/exa/resources/find_similar_result.rb +28 -0
- data/lib/exa/resources/research_list.rb +18 -0
- data/lib/exa/resources/research_task.rb +39 -0
- data/lib/exa/resources/search_result.rb +30 -0
- data/lib/exa/services/answer.rb +23 -0
- data/lib/exa/services/context.rb +27 -0
- data/lib/exa/services/find_similar.rb +26 -0
- data/lib/exa/services/get_contents.rb +25 -0
- data/lib/exa/services/research_get.rb +30 -0
- data/lib/exa/services/research_list.rb +37 -0
- data/lib/exa/services/research_start.rb +26 -0
- data/lib/exa/services/search.rb +26 -0
- data/lib/exa/version.rb +5 -0
- data/lib/exa.rb +54 -0
- metadata +174 -0
@@ -0,0 +1,175 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Set up load paths
|
5
|
+
require "bundler/setup"
|
6
|
+
require "exa"
|
7
|
+
|
8
|
+
# Parse command-line arguments
|
9
|
+
def parse_args(argv)
|
10
|
+
args = {
|
11
|
+
output_format: "json",
|
12
|
+
api_key: nil,
|
13
|
+
events: false,
|
14
|
+
wait: false
|
15
|
+
}
|
16
|
+
|
17
|
+
i = 0
|
18
|
+
while i < argv.length
|
19
|
+
arg = argv[i]
|
20
|
+
case arg
|
21
|
+
when "--instructions"
|
22
|
+
args[:instructions] = argv[i + 1]
|
23
|
+
i += 2
|
24
|
+
when "--model"
|
25
|
+
args[:model] = argv[i + 1]
|
26
|
+
i += 2
|
27
|
+
when "--output-schema"
|
28
|
+
args[:output_schema] = argv[i + 1]
|
29
|
+
i += 2
|
30
|
+
when "--wait"
|
31
|
+
args[:wait] = true
|
32
|
+
i += 1
|
33
|
+
when "--events"
|
34
|
+
args[:events] = true
|
35
|
+
i += 1
|
36
|
+
when "--api-key"
|
37
|
+
args[:api_key] = argv[i + 1]
|
38
|
+
i += 2
|
39
|
+
when "--output-format"
|
40
|
+
args[:output_format] = argv[i + 1]
|
41
|
+
i += 2
|
42
|
+
when "--help", "-h"
|
43
|
+
puts <<~HELP
|
44
|
+
Usage: exa-api research-start --instructions "TEXT" [OPTIONS]
|
45
|
+
|
46
|
+
Start a research task using Exa AI
|
47
|
+
|
48
|
+
Options:
|
49
|
+
--instructions TEXT Research instructions (required)
|
50
|
+
--model MODEL Model to use (e.g., gpt-4, gpt-3.5-turbo)
|
51
|
+
--output-schema JSON JSON schema string for structured output
|
52
|
+
--wait Wait for task to complete (polls until done)
|
53
|
+
--events Include event log in output (only with --wait)
|
54
|
+
--api-key KEY Exa API key (or set EXA_API_KEY env var)
|
55
|
+
--output-format FMT Output format: json, pretty, or text (default: json)
|
56
|
+
--help, -h Show this help message
|
57
|
+
|
58
|
+
Examples:
|
59
|
+
exa-api research-start --instructions "Find Ruby performance tips"
|
60
|
+
exa-api research-start --instructions "Analyze AI trends" --wait --events
|
61
|
+
exa-api research-start --instructions "Summarize papers" --model gpt-4 --wait
|
62
|
+
exa-api research-start --instructions "Find stats" --output-schema '{"type":"object"}'
|
63
|
+
HELP
|
64
|
+
exit 0
|
65
|
+
else
|
66
|
+
i += 1
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
args
|
71
|
+
end
|
72
|
+
|
73
|
+
# Main execution
|
74
|
+
begin
|
75
|
+
args = parse_args(ARGV)
|
76
|
+
|
77
|
+
# Validate instructions
|
78
|
+
if args[:instructions].nil? || args[:instructions].empty?
|
79
|
+
$stderr.puts "Error: --instructions flag is required"
|
80
|
+
$stderr.puts "Run 'exa-api research-start --help' for usage information"
|
81
|
+
exit 1
|
82
|
+
end
|
83
|
+
|
84
|
+
# Resolve API key
|
85
|
+
api_key = Exa::CLI::Base.resolve_api_key(args[:api_key])
|
86
|
+
|
87
|
+
# Resolve output format
|
88
|
+
output_format = Exa::CLI::Base.resolve_output_format(args[:output_format])
|
89
|
+
|
90
|
+
# Build client
|
91
|
+
client = Exa::CLI::Base.build_client(api_key)
|
92
|
+
|
93
|
+
# Prepare research parameters
|
94
|
+
research_params = { instructions: args[:instructions] }
|
95
|
+
research_params[:model] = args[:model] if args[:model]
|
96
|
+
|
97
|
+
# Parse output_schema as JSON if provided
|
98
|
+
if args[:output_schema]
|
99
|
+
begin
|
100
|
+
research_params[:output_schema] = JSON.parse(args[:output_schema])
|
101
|
+
rescue JSON::ParserError => e
|
102
|
+
$stderr.puts "Error: Invalid JSON in --output-schema: #{e.message}"
|
103
|
+
exit 1
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Start research task
|
108
|
+
task = client.research_start(**research_params)
|
109
|
+
|
110
|
+
# If --wait flag is set, poll until task completes
|
111
|
+
if args[:wait]
|
112
|
+
$stderr.print "Starting research task... "
|
113
|
+
|
114
|
+
begin
|
115
|
+
final_task = Exa::CLI::Polling.poll(max_duration: 300, initial_delay: 2, max_delay: 10) do
|
116
|
+
# Get current task status
|
117
|
+
current_task = client.research_get(task.research_id, events: args[:events])
|
118
|
+
|
119
|
+
# Show progress indicator
|
120
|
+
case current_task.status
|
121
|
+
when "pending"
|
122
|
+
$stderr.print "⏳"
|
123
|
+
when "running"
|
124
|
+
$stderr.print "⚙️"
|
125
|
+
end
|
126
|
+
|
127
|
+
# Check if done
|
128
|
+
done = current_task.finished?
|
129
|
+
|
130
|
+
{ done: done, result: current_task, status: current_task.status }
|
131
|
+
end
|
132
|
+
|
133
|
+
$stderr.puts " #{final_task.status.upcase}"
|
134
|
+
|
135
|
+
# Format and output final result
|
136
|
+
output = Exa::CLI::Formatters::ResearchFormatter.format_task(final_task, output_format, show_events: args[:events])
|
137
|
+
puts output
|
138
|
+
|
139
|
+
# Exit with error code if task failed
|
140
|
+
exit 1 if final_task.failed?
|
141
|
+
|
142
|
+
rescue Exa::CLI::Polling::TimeoutError => e
|
143
|
+
$stderr.puts "\nError: Task did not complete within timeout period (5 minutes)"
|
144
|
+
$stderr.puts "Task ID: #{task.research_id}"
|
145
|
+
$stderr.puts "You can check the status later with: exa-api research-get #{task.research_id}"
|
146
|
+
exit 1
|
147
|
+
end
|
148
|
+
else
|
149
|
+
# Just return the initial task (with status "pending")
|
150
|
+
output = Exa::CLI::Formatters::ResearchFormatter.format_task(task, output_format, show_events: false)
|
151
|
+
puts output
|
152
|
+
end
|
153
|
+
|
154
|
+
rescue Exa::ConfigurationError => e
|
155
|
+
$stderr.puts "Configuration error: #{e.message}"
|
156
|
+
exit 1
|
157
|
+
rescue Exa::Unauthorized => e
|
158
|
+
$stderr.puts "Authentication error: #{e.message}"
|
159
|
+
$stderr.puts "Check your API key (set EXA_API_KEY or use --api-key)"
|
160
|
+
exit 1
|
161
|
+
rescue Exa::ClientError => e
|
162
|
+
$stderr.puts "Client error: #{e.message}"
|
163
|
+
exit 1
|
164
|
+
rescue Exa::ServerError => e
|
165
|
+
$stderr.puts "Server error: #{e.message}"
|
166
|
+
$stderr.puts "The Exa API may be experiencing issues. Please try again later."
|
167
|
+
exit 1
|
168
|
+
rescue Exa::Error => e
|
169
|
+
$stderr.puts "Error: #{e.message}"
|
170
|
+
exit 1
|
171
|
+
rescue StandardError => e
|
172
|
+
$stderr.puts "Unexpected error: #{e.message}"
|
173
|
+
$stderr.puts e.backtrace.first(5) if ENV["DEBUG"]
|
174
|
+
exit 1
|
175
|
+
end
|
data/exe/exa-ai-search
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Set up load paths
|
5
|
+
require "bundler/setup"
|
6
|
+
require "exa"
|
7
|
+
|
8
|
+
# Parse command-line arguments
|
9
|
+
def parse_args(argv)
|
10
|
+
args = {
|
11
|
+
output_format: "json",
|
12
|
+
api_key: nil
|
13
|
+
}
|
14
|
+
|
15
|
+
# Extract query (first non-flag argument)
|
16
|
+
query_parts = []
|
17
|
+
i = 0
|
18
|
+
while i < argv.length
|
19
|
+
arg = argv[i]
|
20
|
+
case arg
|
21
|
+
when "--num-results"
|
22
|
+
args[:num_results] = argv[i + 1].to_i
|
23
|
+
i += 2
|
24
|
+
when "--type"
|
25
|
+
args[:type] = argv[i + 1]
|
26
|
+
i += 2
|
27
|
+
when "--include-domains"
|
28
|
+
args[:include_domains] = argv[i + 1].split(",").map(&:strip)
|
29
|
+
i += 2
|
30
|
+
when "--exclude-domains"
|
31
|
+
args[:exclude_domains] = argv[i + 1].split(",").map(&:strip)
|
32
|
+
i += 2
|
33
|
+
when "--use-autoprompt"
|
34
|
+
args[:use_autoprompt] = true
|
35
|
+
i += 1
|
36
|
+
when "--api-key"
|
37
|
+
args[:api_key] = argv[i + 1]
|
38
|
+
i += 2
|
39
|
+
when "--output-format"
|
40
|
+
args[:output_format] = argv[i + 1]
|
41
|
+
i += 2
|
42
|
+
when "--help", "-h"
|
43
|
+
puts <<~HELP
|
44
|
+
Usage: exa-api search QUERY [OPTIONS]
|
45
|
+
|
46
|
+
Search the web using Exa AI
|
47
|
+
|
48
|
+
Arguments:
|
49
|
+
QUERY Search query (required)
|
50
|
+
|
51
|
+
Options:
|
52
|
+
--num-results N Number of results to return (default: 10)
|
53
|
+
--type TYPE Search type: keyword, neural, or auto (default: auto)
|
54
|
+
--include-domains D Comma-separated list of domains to include
|
55
|
+
--exclude-domains D Comma-separated list of domains to exclude
|
56
|
+
--use-autoprompt Use Exa's autoprompt feature
|
57
|
+
--api-key KEY Exa API key (or set EXA_API_KEY env var)
|
58
|
+
--output-format FMT Output format: json, pretty, or text (default: json)
|
59
|
+
--help, -h Show this help message
|
60
|
+
|
61
|
+
Examples:
|
62
|
+
exa-api search "ruby programming"
|
63
|
+
exa-api search "machine learning" --num-results 5 --type keyword
|
64
|
+
exa-api search "AI research" --include-domains arxiv.org,scholar.google.com
|
65
|
+
exa-api search "tutorials" --output-format pretty
|
66
|
+
HELP
|
67
|
+
exit 0
|
68
|
+
else
|
69
|
+
query_parts << arg
|
70
|
+
i += 1
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
args[:query] = query_parts.join(" ")
|
75
|
+
args
|
76
|
+
end
|
77
|
+
|
78
|
+
# Main execution
|
79
|
+
begin
|
80
|
+
args = parse_args(ARGV)
|
81
|
+
|
82
|
+
# Validate query
|
83
|
+
if args[:query].nil? || args[:query].empty?
|
84
|
+
$stderr.puts "Error: Query is required"
|
85
|
+
$stderr.puts "Run 'exa-api search --help' for usage information"
|
86
|
+
exit 1
|
87
|
+
end
|
88
|
+
|
89
|
+
# Resolve API key
|
90
|
+
api_key = Exa::CLI::Base.resolve_api_key(args[:api_key])
|
91
|
+
|
92
|
+
# Resolve output format
|
93
|
+
output_format = Exa::CLI::Base.resolve_output_format(args[:output_format])
|
94
|
+
|
95
|
+
# Build client
|
96
|
+
client = Exa::CLI::Base.build_client(api_key)
|
97
|
+
|
98
|
+
# Prepare search parameters
|
99
|
+
search_params = {}
|
100
|
+
search_params[:num_results] = args[:num_results] if args[:num_results]
|
101
|
+
search_params[:type] = args[:type] if args[:type]
|
102
|
+
search_params[:include_domains] = args[:include_domains] if args[:include_domains]
|
103
|
+
search_params[:exclude_domains] = args[:exclude_domains] if args[:exclude_domains]
|
104
|
+
search_params[:use_autoprompt] = args[:use_autoprompt] if args[:use_autoprompt]
|
105
|
+
|
106
|
+
# Execute search
|
107
|
+
result = client.search(args[:query], **search_params)
|
108
|
+
|
109
|
+
# Format and output result
|
110
|
+
output = Exa::CLI::Formatters::SearchFormatter.format(result, output_format)
|
111
|
+
puts output
|
112
|
+
|
113
|
+
rescue Exa::ConfigurationError => e
|
114
|
+
$stderr.puts "Configuration error: #{e.message}"
|
115
|
+
exit 1
|
116
|
+
rescue Exa::Unauthorized => e
|
117
|
+
$stderr.puts "Authentication error: #{e.message}"
|
118
|
+
$stderr.puts "Check your API key (set EXA_API_KEY or use --api-key)"
|
119
|
+
exit 1
|
120
|
+
rescue Exa::ClientError => e
|
121
|
+
$stderr.puts "Client error: #{e.message}"
|
122
|
+
exit 1
|
123
|
+
rescue Exa::ServerError => e
|
124
|
+
$stderr.puts "Server error: #{e.message}"
|
125
|
+
$stderr.puts "The Exa API may be experiencing issues. Please try again later."
|
126
|
+
exit 1
|
127
|
+
rescue Exa::Error => e
|
128
|
+
$stderr.puts "Error: #{e.message}"
|
129
|
+
exit 1
|
130
|
+
rescue StandardError => e
|
131
|
+
$stderr.puts "Unexpected error: #{e.message}"
|
132
|
+
$stderr.puts e.backtrace.first(5) if ENV["DEBUG"]
|
133
|
+
exit 1
|
134
|
+
end
|
data/lib/exa/cli/base.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Exa
|
4
|
+
module CLI
|
5
|
+
class Base
|
6
|
+
# Resolve API key from flag or environment variable
|
7
|
+
# Flag takes precedence over environment variable
|
8
|
+
def self.resolve_api_key(flag_value)
|
9
|
+
return flag_value if flag_value && !flag_value.empty?
|
10
|
+
|
11
|
+
env_key = ENV["EXA_API_KEY"]
|
12
|
+
return env_key if env_key && !env_key.empty?
|
13
|
+
|
14
|
+
raise ConfigurationError,
|
15
|
+
"Missing API key. Set EXA_API_KEY environment variable or use --api-key flag"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Resolve and validate output format
|
19
|
+
# Valid formats: json, pretty, text
|
20
|
+
# Defaults to json
|
21
|
+
def self.resolve_output_format(flag_value)
|
22
|
+
format = (flag_value || "json").downcase
|
23
|
+
valid_formats = %w[json pretty text]
|
24
|
+
|
25
|
+
return format if valid_formats.include?(format)
|
26
|
+
|
27
|
+
raise ConfigurationError,
|
28
|
+
"Invalid output format: #{format}. Valid formats: #{valid_formats.join(', ')}"
|
29
|
+
end
|
30
|
+
|
31
|
+
# Build a client instance with the given API key
|
32
|
+
def self.build_client(api_key, **options)
|
33
|
+
Client.new(api_key: api_key, **options)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Format output data based on format type
|
37
|
+
def self.format_output(data, format)
|
38
|
+
case format
|
39
|
+
when "json"
|
40
|
+
JSON.pretty_generate(data.is_a?(Hash) ? data : { result: data })
|
41
|
+
when "pretty"
|
42
|
+
data.inspect
|
43
|
+
when "text"
|
44
|
+
data.to_s
|
45
|
+
else
|
46
|
+
data.to_s
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Exa
|
4
|
+
module CLI
|
5
|
+
class ErrorHandler
|
6
|
+
def self.handle_error(error, command_name = nil)
|
7
|
+
case error
|
8
|
+
when ConfigurationError
|
9
|
+
handle_configuration_error(error, command_name)
|
10
|
+
when Unauthorized
|
11
|
+
handle_unauthorized_error(error, command_name)
|
12
|
+
when ClientError
|
13
|
+
handle_client_error(error, command_name)
|
14
|
+
when ServerError
|
15
|
+
handle_server_error(error, command_name)
|
16
|
+
else
|
17
|
+
handle_generic_error(error, command_name)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def self.handle_configuration_error(error, command_name)
|
24
|
+
$stderr.puts "❌ Configuration Error"
|
25
|
+
$stderr.puts ""
|
26
|
+
$stderr.puts error.message
|
27
|
+
$stderr.puts ""
|
28
|
+
$stderr.puts "Solutions:"
|
29
|
+
$stderr.puts " 1. Set the EXA_API_KEY environment variable:"
|
30
|
+
$stderr.puts " export EXA_API_KEY='your-api-key'"
|
31
|
+
$stderr.puts ""
|
32
|
+
$stderr.puts " 2. Or pass it as a flag:"
|
33
|
+
$stderr.puts " #{command_name} ... --api-key YOUR_API_KEY" if command_name
|
34
|
+
$stderr.puts ""
|
35
|
+
$stderr.puts "Get your API key at: https://dashboard.exa.ai"
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.handle_unauthorized_error(error, command_name)
|
39
|
+
$stderr.puts "❌ Authentication Error"
|
40
|
+
$stderr.puts ""
|
41
|
+
$stderr.puts "Your API key is invalid or has expired."
|
42
|
+
$stderr.puts ""
|
43
|
+
if error.response&.fetch("error", nil)
|
44
|
+
$stderr.puts "Details: #{error.response['error']}"
|
45
|
+
$stderr.puts ""
|
46
|
+
end
|
47
|
+
$stderr.puts "Solutions:"
|
48
|
+
$stderr.puts " 1. Verify your API key is correct"
|
49
|
+
$stderr.puts " 2. Check if your API key has expired or been revoked"
|
50
|
+
$stderr.puts " 3. Get a new key from: https://dashboard.exa.ai"
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.handle_client_error(error, command_name)
|
54
|
+
$stderr.puts "❌ Request Error"
|
55
|
+
$stderr.puts ""
|
56
|
+
$stderr.puts error.message
|
57
|
+
$stderr.puts ""
|
58
|
+
|
59
|
+
# Try to extract status code from response
|
60
|
+
status = error.response&.fetch("status", "unknown") if error.response.is_a?(Hash)
|
61
|
+
|
62
|
+
case status
|
63
|
+
when 400
|
64
|
+
$stderr.puts "This was a bad request. Please check your arguments."
|
65
|
+
when 404
|
66
|
+
$stderr.puts "The requested resource was not found."
|
67
|
+
when 422
|
68
|
+
$stderr.puts "The request data was invalid. Check your parameters."
|
69
|
+
when 429
|
70
|
+
$stderr.puts "You've exceeded the rate limit. Please wait before trying again."
|
71
|
+
end
|
72
|
+
|
73
|
+
$stderr.puts ""
|
74
|
+
$stderr.puts "Run '#{command_name} --help' for usage information." if command_name
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.handle_server_error(error, command_name)
|
78
|
+
$stderr.puts "❌ Server Error"
|
79
|
+
$stderr.puts ""
|
80
|
+
$stderr.puts "The Exa API encountered an error:"
|
81
|
+
$stderr.puts error.message
|
82
|
+
$stderr.puts ""
|
83
|
+
$stderr.puts "Solutions:"
|
84
|
+
$stderr.puts " 1. Try again in a moment"
|
85
|
+
$stderr.puts " 2. Check API status: https://status.exa.ai"
|
86
|
+
$stderr.puts " 3. Contact support if the error persists"
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.handle_generic_error(error, command_name)
|
90
|
+
$stderr.puts "❌ Error"
|
91
|
+
$stderr.puts ""
|
92
|
+
$stderr.puts error.message
|
93
|
+
$stderr.puts ""
|
94
|
+
$stderr.puts "Run '#{command_name} --help' for usage information." if command_name
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Exa
|
4
|
+
module CLI
|
5
|
+
module Formatters
|
6
|
+
class AnswerFormatter
|
7
|
+
def self.format(result, format)
|
8
|
+
case format
|
9
|
+
when "json"
|
10
|
+
JSON.pretty_generate(result.to_h)
|
11
|
+
when "pretty"
|
12
|
+
format_pretty(result)
|
13
|
+
when "text"
|
14
|
+
format_text(result)
|
15
|
+
else
|
16
|
+
JSON.pretty_generate(result.to_h)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def self.format_pretty(result)
|
23
|
+
output = []
|
24
|
+
output << "Answer:"
|
25
|
+
output << "-" * 60
|
26
|
+
|
27
|
+
# Handle both string and structured (hash) answers
|
28
|
+
if result.answer.is_a?(Hash)
|
29
|
+
output << JSON.pretty_generate(result.answer)
|
30
|
+
else
|
31
|
+
output << result.answer
|
32
|
+
end
|
33
|
+
output << ""
|
34
|
+
|
35
|
+
if result.citations && !result.citations.empty?
|
36
|
+
output << "Citations:"
|
37
|
+
output << "-" * 60
|
38
|
+
result.citations.each_with_index do |citation, idx|
|
39
|
+
output << "[#{idx + 1}] #{citation['title']}"
|
40
|
+
output << " URL: #{citation['url']}"
|
41
|
+
output << " Author: #{citation['author']}" if citation['author']
|
42
|
+
output << " Date: #{citation['publishedDate']}" if citation['publishedDate']
|
43
|
+
output << ""
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
output << "Cost: $#{result.cost_dollars}" if result.cost_dollars
|
48
|
+
|
49
|
+
output.join("\n")
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.format_text(result)
|
53
|
+
# Handle both string and structured (hash) answers
|
54
|
+
if result.answer.is_a?(Hash)
|
55
|
+
JSON.pretty_generate(result.answer)
|
56
|
+
else
|
57
|
+
result.answer
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Exa
|
4
|
+
module CLI
|
5
|
+
module Formatters
|
6
|
+
class ContentsFormatter
|
7
|
+
def self.format(result, format)
|
8
|
+
case format
|
9
|
+
when "json"
|
10
|
+
JSON.pretty_generate(result.to_h)
|
11
|
+
when "pretty"
|
12
|
+
format_pretty(result)
|
13
|
+
when "text"
|
14
|
+
format_text(result)
|
15
|
+
else
|
16
|
+
JSON.pretty_generate(result.to_h)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def self.format_pretty(result)
|
23
|
+
output = []
|
24
|
+
result.results.each_with_index do |content, idx|
|
25
|
+
output << "=== Content #{idx + 1} ==="
|
26
|
+
output << "URL: #{content['url']}"
|
27
|
+
output << "Title: #{content['title']}"
|
28
|
+
output << ""
|
29
|
+
output << "Text:"
|
30
|
+
output << "-" * 40
|
31
|
+
text = content['text'] || content['content'] || "(No text available)"
|
32
|
+
# Truncate long text to first 500 chars
|
33
|
+
truncated = text.length > 500 ? text[0..500] + "..." : text
|
34
|
+
output << truncated
|
35
|
+
output << ""
|
36
|
+
end
|
37
|
+
output.join("\n")
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.format_text(result)
|
41
|
+
output = []
|
42
|
+
result.results.each do |content|
|
43
|
+
output << "#{content['url']}\n#{content['text'] || '(No text available)'}"
|
44
|
+
end
|
45
|
+
output.join("\n\n")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Exa
|
6
|
+
module CLI
|
7
|
+
module Formatters
|
8
|
+
class ContextFormatter
|
9
|
+
def self.format(result, format)
|
10
|
+
case format
|
11
|
+
when "json"
|
12
|
+
JSON.pretty_generate(result.to_h)
|
13
|
+
when "pretty"
|
14
|
+
format_pretty(result)
|
15
|
+
when "text"
|
16
|
+
format_text(result)
|
17
|
+
else
|
18
|
+
JSON.pretty_generate(result.to_h)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def self.format_pretty(result)
|
25
|
+
output = []
|
26
|
+
output << "=" * 60
|
27
|
+
output << "Query: #{result.query}"
|
28
|
+
output << "=" * 60
|
29
|
+
output << ""
|
30
|
+
output << "Metadata:"
|
31
|
+
output << " Request ID: #{result.request_id}"
|
32
|
+
output << " Results: #{result.results_count}"
|
33
|
+
output << " Cost: $#{result.cost_dollars}"
|
34
|
+
output << " Search Time: #{result.search_time}ms"
|
35
|
+
output << ""
|
36
|
+
output << "Code Context:"
|
37
|
+
output << "-" * 60
|
38
|
+
output << result.response.to_s
|
39
|
+
output.join("\n")
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.format_text(result)
|
43
|
+
output = []
|
44
|
+
output << "Query: #{result.query}"
|
45
|
+
output << "Request ID: #{result.request_id}"
|
46
|
+
output << "Results: #{result.results_count}"
|
47
|
+
output << "Cost: $#{result.cost_dollars}"
|
48
|
+
output << "Search Time: #{result.search_time}ms"
|
49
|
+
output << ""
|
50
|
+
output << "Code Context:"
|
51
|
+
output << "-" * 40
|
52
|
+
output << result.response.to_s
|
53
|
+
output.join("\n")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|