exa-ai 0.2.0 → 0.3.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.
data/exe/exa-ai CHANGED
@@ -1,9 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "bundler/setup"
5
- require "exa"
6
- require "exa/cli/base"
4
+ require "exa-ai"
7
5
 
8
6
  # Global CLI interface for Exa API
9
7
 
data/exe/exa-ai-answer CHANGED
@@ -1,18 +1,15 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # Set up load paths
5
- require "bundler/setup"
6
- require "exa"
7
- require "exa/cli/base"
8
- require "exa/cli/formatters/answer_formatter"
4
+ require "exa-ai"
9
5
 
10
6
  # Parse command-line arguments
11
7
  def parse_args(argv)
12
8
  args = {
13
9
  output_format: "json",
14
10
  api_key: nil,
15
- text: false
11
+ text: false,
12
+ stream: false
16
13
  }
17
14
 
18
15
  # Extract query (first non-flag argument)
@@ -24,9 +21,15 @@ def parse_args(argv)
24
21
  when "--text"
25
22
  args[:text] = true
26
23
  i += 1
24
+ when "--stream"
25
+ args[:stream] = true
26
+ i += 1
27
27
  when "--output-schema"
28
28
  args[:output_schema] = argv[i + 1]
29
29
  i += 2
30
+ when "--system-prompt"
31
+ args[:system_prompt] = argv[i + 1]
32
+ i += 2
30
33
  when "--api-key"
31
34
  args[:api_key] = argv[i + 1]
32
35
  i += 2
@@ -43,17 +46,21 @@ def parse_args(argv)
43
46
  QUERY Question or query to answer (required)
44
47
 
45
48
  Options:
49
+ --stream Stream answer chunks in real-time
46
50
  --text Include full text content from sources
47
51
  --output-schema JSON JSON schema for structured output
52
+ --system-prompt TEXT System prompt to guide answer generation
48
53
  --api-key KEY Exa API key (or set EXA_API_KEY env var)
49
54
  --output-format FMT Output format: json, pretty, or text (default: json)
50
55
  --help, -h Show this help message
51
56
 
52
57
  Examples:
53
58
  exa-api answer "What is the capital of France?"
59
+ exa-api answer "Latest AI breakthroughs" --stream
54
60
  exa-api answer "Latest AI breakthroughs" --text
55
61
  exa-api answer "Ruby best practices" --output-format pretty
56
62
  exa-api answer "What is the capital of France?" --output-schema '{"type":"object","properties":{"city":{"type":"string"},"state":{"type":"string"}}}'
63
+ exa-api answer "What is Paris?" --system-prompt "Respond in the voice of a pirate"
57
64
  HELP
58
65
  exit 0
59
66
  else
@@ -89,6 +96,7 @@ begin
89
96
  # Prepare answer parameters
90
97
  answer_params = {}
91
98
  answer_params[:text] = args[:text] if args[:text]
99
+ answer_params[:system_prompt] = args[:system_prompt] if args[:system_prompt]
92
100
 
93
101
  # Parse output_schema as JSON if provided
94
102
  if args[:output_schema]
@@ -100,12 +108,24 @@ begin
100
108
  end
101
109
  end
102
110
 
103
- # Execute answer
104
- result = client.answer(args[:query], **answer_params)
105
-
106
- # Format and output result
107
- output = Exa::CLI::Formatters::AnswerFormatter.format(result, output_format)
108
- puts output
111
+ # Execute answer or streaming answer
112
+ if args[:stream]
113
+ # Streaming mode - output chunks in real-time
114
+ client.answer_stream(args[:query], **answer_params) do |chunk|
115
+ # Extract content from the streaming response format
116
+ # API returns: {"choices":[{"delta":{"content":"..."}}]}
117
+ if chunk["choices"]&.first&.dig("delta", "content")
118
+ print chunk["choices"][0]["delta"]["content"]
119
+ $stdout.flush
120
+ end
121
+ end
122
+ puts # Add newline at the end
123
+ else
124
+ # Non-streaming mode - collect full response and format
125
+ result = client.answer(args[:query], **answer_params)
126
+ output = Exa::CLI::Formatters::AnswerFormatter.format(result, output_format)
127
+ puts output
128
+ end
109
129
 
110
130
  rescue Exa::ConfigurationError => e
111
131
  $stderr.puts "Configuration error: #{e.message}"
data/exe/exa-ai-context CHANGED
@@ -1,10 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "bundler/setup"
5
- require "exa"
6
- require "exa/cli/base"
7
- require "exa/cli/formatters/context_formatter"
4
+ require "exa-ai"
8
5
 
9
6
  # Parse command line arguments
10
7
  def parse_args(args)
@@ -1,18 +1,25 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "bundler/setup"
5
- require "exa"
6
- require "exa/cli/base"
7
- require "exa/cli/formatters/contents_formatter"
4
+ require "exa-ai"
8
5
 
9
6
  # Parse command line arguments
10
7
  def parse_args(args)
11
8
  ids = []
12
9
  api_key = nil
13
10
  text = false
14
- highlights = false
11
+ text_max_characters = nil
12
+ include_html_tags = false
15
13
  summary = false
14
+ summary_query = nil
15
+ summary_schema = nil
16
+ subpages = nil
17
+ subpage_target = []
18
+ links = nil
19
+ image_links = nil
20
+ context = false
21
+ context_max_characters = nil
22
+ livecrawl_timeout = nil
16
23
  output_format = nil
17
24
 
18
25
  i = 0
@@ -25,10 +32,48 @@ def parse_args(args)
25
32
  api_key = args[i]
26
33
  when "--text"
27
34
  text = true
28
- when "--highlights"
29
- highlights = true
35
+ i += 1
36
+ when "--text-max-characters"
37
+ i += 1
38
+ text_max_characters = args[i].to_i
39
+ when "--include-html-tags"
40
+ include_html_tags = true
41
+ i += 1
30
42
  when "--summary"
31
43
  summary = true
44
+ i += 1
45
+ when "--summary-query"
46
+ i += 1
47
+ summary_query = args[i]
48
+ when "--summary-schema"
49
+ i += 1
50
+ schema_arg = args[i]
51
+ summary_schema = if schema_arg.start_with?("@")
52
+ JSON.parse(File.read(schema_arg[1..]))
53
+ else
54
+ JSON.parse(schema_arg)
55
+ end
56
+ when "--subpages"
57
+ i += 1
58
+ subpages = args[i].to_i
59
+ when "--subpage-target"
60
+ i += 1
61
+ subpage_target << args[i]
62
+ when "--links"
63
+ i += 1
64
+ links = args[i].to_i
65
+ when "--image-links"
66
+ i += 1
67
+ image_links = args[i].to_i
68
+ when "--context"
69
+ context = true
70
+ i += 1
71
+ when "--context-max-characters"
72
+ i += 1
73
+ context_max_characters = args[i].to_i
74
+ when "--livecrawl-timeout"
75
+ i += 1
76
+ livecrawl_timeout = args[i].to_i
32
77
  when "--output-format"
33
78
  i += 1
34
79
  output_format = args[i]
@@ -41,34 +86,130 @@ def parse_args(args)
41
86
  ids_arg = arg
42
87
  ids = ids_arg.include?(",") ? ids_arg.split(",").map(&:strip) : [ids_arg]
43
88
  end
89
+ i += 1
90
+ end
91
+ end
92
+
93
+ {
94
+ ids: ids,
95
+ api_key: api_key,
96
+ text: text,
97
+ text_max_characters: text_max_characters,
98
+ include_html_tags: include_html_tags,
99
+ summary: summary,
100
+ summary_query: summary_query,
101
+ summary_schema: summary_schema,
102
+ subpages: subpages,
103
+ subpage_target: subpage_target,
104
+ links: links,
105
+ image_links: image_links,
106
+ context: context,
107
+ context_max_characters: context_max_characters,
108
+ livecrawl_timeout: livecrawl_timeout,
109
+ output_format: output_format
110
+ }
111
+ end
112
+
113
+ # Build contents parameters from extracted flags
114
+ def build_contents_params(args)
115
+ params = {}
116
+
117
+ # Text options
118
+ if args[:text]
119
+ if args[:text_max_characters] || args[:include_html_tags]
120
+ params[:text] = {}
121
+ params[:text][:max_characters] = args[:text_max_characters] if args[:text_max_characters]
122
+ params[:text][:include_html_tags] = args[:include_html_tags] if args[:include_html_tags]
123
+ else
124
+ params[:text] = true
125
+ end
126
+ end
127
+
128
+ # Summary options
129
+ if args[:summary]
130
+ if args[:summary_query] || args[:summary_schema]
131
+ params[:summary] = {}
132
+ params[:summary][:query] = args[:summary_query] if args[:summary_query]
133
+ params[:summary][:schema] = args[:summary_schema] if args[:summary_schema]
134
+ else
135
+ params[:summary] = true
136
+ end
137
+ end
138
+
139
+ # Context options
140
+ if args[:context]
141
+ if args[:context_max_characters]
142
+ params[:context] = { max_characters: args[:context_max_characters] }
143
+ else
144
+ params[:context] = true
44
145
  end
146
+ end
147
+
148
+ # Subpages options
149
+ params[:subpages] = args[:subpages] if args[:subpages]
150
+ params[:subpage_target] = args[:subpage_target] if args[:subpage_target].any?
45
151
 
46
- i += 1
152
+ # Extras options
153
+ if args[:links] || args[:image_links]
154
+ params[:extras] = {}
155
+ params[:extras][:links] = args[:links] if args[:links]
156
+ params[:extras][:image_links] = args[:image_links] if args[:image_links]
47
157
  end
48
158
 
49
- { ids: ids, api_key: api_key, text: text, highlights: highlights, summary: summary, output_format: output_format }
159
+ # Livecrawl options
160
+ params[:livecrawl_timeout] = args[:livecrawl_timeout] if args[:livecrawl_timeout]
161
+
162
+ params.empty? ? nil : params
50
163
  end
51
164
 
52
165
  def print_help
53
- puts "Exa Get-Contents - Retrieve page contents"
54
- puts ""
55
- puts "Usage: exa-api get-contents <ids> [options]"
56
- puts ""
57
- puts "Arguments:"
58
- puts " ids Comma-separated list of IDs or URLs (required)"
59
- puts ""
60
- puts "Options:"
61
- puts " --text Include page text in response"
62
- puts " --highlights Include highlights in response"
63
- puts " --summary Include summary in response"
64
- puts " --api-key KEY Exa API key (or set EXA_API_KEY env var)"
65
- puts " --output-format FMT Output format: json, pretty, or text (default: json)"
66
- puts " --help, -h Show this help message"
67
- puts ""
68
- puts "Examples:"
69
- puts " exa-api get-contents 'https://example.com'"
70
- puts " exa-api get-contents 'id1,id2,id3' --text"
71
- puts " exa-api get-contents 'https://example.com' --highlights --output-format pretty"
166
+ puts <<~HELP
167
+ Usage: exa-api get-contents <urls> [options]
168
+
169
+ Retrieve full page contents from URLs
170
+
171
+ Arguments:
172
+ urls Comma-separated list of URLs (required)
173
+
174
+ Options:
175
+ Text Extraction:
176
+ --text Include page text in response
177
+ --text-max-characters N Max characters for page text
178
+ --include-html-tags Include HTML tags in text extraction
179
+
180
+ Summary:
181
+ --summary Include AI-generated summary
182
+ --summary-query PROMPT Custom prompt for summary generation
183
+ --summary-schema FILE JSON schema for summary structure (@file syntax)
184
+
185
+ Context:
186
+ --context Format results as context for LLM RAG
187
+ --context-max-characters N Max characters for context string
188
+
189
+ Subpages:
190
+ --subpages N Number of subpages to crawl
191
+ --subpage-target PHRASE Subpage target phrases (repeatable)
192
+
193
+ Extras:
194
+ --links N Number of links to extract per result
195
+ --image-links N Number of image links to extract
196
+
197
+ Livecrawl:
198
+ --livecrawl-timeout N Timeout for livecrawling in milliseconds
199
+
200
+ General:
201
+ --api-key KEY Exa API key (or set EXA_API_KEY env var)
202
+ --output-format FMT Output format: json, pretty, or text (default: json)
203
+ --help, -h Show this help message
204
+
205
+ Examples:
206
+ exa-api get-contents 'https://example.com'
207
+ exa-api get-contents 'https://example.com' --text
208
+ exa-api get-contents 'https://example.com' --text --text-max-characters 3000 --include-html-tags
209
+ exa-api get-contents 'url1,url2' --summary --summary-query "Be terse"
210
+ exa-api get-contents 'https://example.com' --subpages 1 --subpage-target about
211
+ exa-api get-contents 'https://example.com' --links 5 --image-links 10
212
+ HELP
72
213
  end
73
214
 
74
215
  begin
@@ -77,9 +218,9 @@ begin
77
218
 
78
219
  # Validate IDs
79
220
  if options[:ids].empty?
80
- puts "Error: IDs argument required"
81
- puts ""
82
- puts "Run 'exa-api get-contents --help' for usage information."
221
+ $stderr.puts "Error: URLs argument required"
222
+ $stderr.puts ""
223
+ $stderr.puts "Run 'exa-api get-contents --help' for usage information."
83
224
  exit 1
84
225
  end
85
226
 
@@ -93,10 +234,8 @@ begin
93
234
  client = Exa::CLI::Base.build_client(api_key)
94
235
 
95
236
  # Build request parameters
96
- params = {}
97
- params[:text] = true if options[:text]
98
- params[:highlights] = true if options[:highlights]
99
- params[:summary] = true if options[:summary]
237
+ params = build_contents_params(options)
238
+ params ||= {}
100
239
 
101
240
  # Call API
102
241
  result = client.get_contents(options[:ids], **params)
@@ -104,11 +243,25 @@ begin
104
243
  # Format and output
105
244
  output = Exa::CLI::Formatters::ContentsFormatter.format(result, output_format)
106
245
  puts output
246
+ rescue Exa::ConfigurationError => e
247
+ $stderr.puts "Configuration error: #{e.message}"
248
+ exit 1
249
+ rescue Exa::Unauthorized => e
250
+ $stderr.puts "Authentication error: #{e.message}"
251
+ $stderr.puts "Check your API key (set EXA_API_KEY or use --api-key)"
252
+ exit 1
253
+ rescue Exa::ClientError => e
254
+ $stderr.puts "Client error: #{e.message}"
255
+ exit 1
256
+ rescue Exa::ServerError => e
257
+ $stderr.puts "Server error: #{e.message}"
258
+ $stderr.puts "The Exa API may be experiencing issues. Please try again later."
259
+ exit 1
107
260
  rescue Exa::Error => e
108
- puts "Error: #{e.message}"
261
+ $stderr.puts "Error: #{e.message}"
109
262
  exit 1
110
263
  rescue StandardError => e
111
- puts "Unexpected error: #{e.message}"
112
- puts e.backtrace.first(5) if ENV["DEBUG"]
264
+ $stderr.puts "Unexpected error: #{e.message}"
265
+ $stderr.puts e.backtrace.first(5) if ENV["DEBUG"]
113
266
  exit 1
114
267
  end
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "bundler/setup"
5
- require "exa"
4
+ require "exa-ai"
6
5
 
7
6
  # Parse command-line arguments
8
7
  api_key = nil
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "bundler/setup"
5
- require "exa"
4
+ require "exa-ai"
6
5
 
7
6
  # Parse command-line arguments
8
7
  api_key = nil
@@ -1,9 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # Set up load paths
5
- require "bundler/setup"
6
- require "exa"
4
+ require "exa-ai"
7
5
 
8
6
  # Parse command-line arguments
9
7
  def parse_args(argv)
@@ -47,7 +45,7 @@ def parse_args(argv)
47
45
 
48
46
  Options:
49
47
  --instructions TEXT Research instructions (required)
50
- --model MODEL Model to use (e.g., gpt-4, gpt-3.5-turbo)
48
+ --model MODEL Research model: exa-research (default), exa-research-pro, exa-research-fast
51
49
  --output-schema JSON JSON schema string for structured output
52
50
  --wait Wait for task to complete (polls until done)
53
51
  --events Include event log in output (only with --wait)
@@ -58,7 +56,7 @@ def parse_args(argv)
58
56
  Examples:
59
57
  exa-api research-start --instructions "Find Ruby performance tips"
60
58
  exa-api research-start --instructions "Analyze AI trends" --wait --events
61
- exa-api research-start --instructions "Summarize papers" --model gpt-4 --wait
59
+ exa-api research-start --instructions "Summarize papers" --model exa-research-pro --wait
62
60
  exa-api research-start --instructions "Find stats" --output-schema '{"type":"object"}'
63
61
  HELP
64
62
  exit 0
data/exe/exa-ai-search CHANGED
@@ -1,9 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # Set up load paths
5
- require "bundler/setup"
6
- require "exa"
4
+ require "exa-ai"
7
5
 
8
6
  # Parse command-line arguments
9
7
  def parse_args(argv)
@@ -39,9 +37,6 @@ def parse_args(argv)
39
37
  when "--exclude-domains"
40
38
  args[:exclude_domains] = argv[i + 1].split(",").map(&:strip)
41
39
  i += 2
42
- when "--use-autoprompt"
43
- args[:use_autoprompt] = true
44
- i += 1
45
40
  when "--api-key"
46
41
  args[:api_key] = argv[i + 1]
47
42
  i += 2
@@ -159,7 +154,6 @@ def parse_args(argv)
159
154
  --image-links N Number of image links to extract
160
155
 
161
156
  General Options:
162
- --use-autoprompt Use Exa's autoprompt feature
163
157
  --linkedin TYPE Search LinkedIn: company, person, or all
164
158
  --api-key KEY Exa API key (or set EXA_API_KEY env var)
165
159
  --output-format FMT Output format: json, pretty, or text (default: json)
@@ -271,7 +265,6 @@ begin
271
265
  search_params[:exclude_text] = args[:exclude_text] if args[:exclude_text]
272
266
  contents = build_contents(args)
273
267
  search_params.merge!(contents) if contents
274
- search_params[:useAutoprompt] = args[:use_autoprompt] if args[:use_autoprompt]
275
268
 
276
269
  # Execute search based on LinkedIn type
277
270
  result = case args[:linkedin]
data/lib/exa/cli/base.rb CHANGED
@@ -11,7 +11,7 @@ module Exa
11
11
  env_key = ENV["EXA_API_KEY"]
12
12
  return env_key if env_key && !env_key.empty?
13
13
 
14
- raise ConfigurationError,
14
+ raise Exa::ConfigurationError,
15
15
  "Missing API key. Set EXA_API_KEY environment variable or use --api-key flag"
16
16
  end
17
17
 
@@ -24,13 +24,13 @@ module Exa
24
24
 
25
25
  return format if valid_formats.include?(format)
26
26
 
27
- raise ConfigurationError,
27
+ raise Exa::ConfigurationError,
28
28
  "Invalid output format: #{format}. Valid formats: #{valid_formats.join(', ')}"
29
29
  end
30
30
 
31
31
  # Build a client instance with the given API key
32
32
  def self.build_client(api_key, **options)
33
- Client.new(api_key: api_key, **options)
33
+ Exa::Client.new(api_key: api_key, **options)
34
34
  end
35
35
 
36
36
  # Format output data based on format type
data/lib/exa/client.rb CHANGED
@@ -71,6 +71,21 @@ module Exa
71
71
  Services::Answer.new(connection, query: query, **options).call
72
72
  end
73
73
 
74
+ # Stream AI-generated answers to a query
75
+ #
76
+ # Returns partial answer chunks as they are generated by the API.
77
+ #
78
+ # @param query [String] Question or query
79
+ # @param options [Hash] Answer options
80
+ # @option options [Boolean] :text Include full text content (default: false)
81
+ # @option options [Hash] :output_schema JSON schema for structured output
82
+ # @yield [chunk] Yields each answer chunk as it arrives
83
+ # @yieldparam chunk [Hash] Partial answer data with {"answer" => "text"}
84
+ # @return [void]
85
+ def answer_stream(query, **options, &block)
86
+ Services::AnswerStream.new(connection, query: query, **options).call(&block)
87
+ end
88
+
74
89
  # Start an asynchronous research task
75
90
  #
76
91
  # @param params [Hash] Research parameters
@@ -146,6 +161,7 @@ module Exa
146
161
  options = {}
147
162
  options[:base_url] = @options[:base_url] if @options[:base_url]
148
163
  options[:timeout] = @options[:timeout] if @options[:timeout]
164
+ options[:debug] = true if ENV["EXA_DEBUG"]
149
165
  options
150
166
  end
151
167
 
@@ -1,17 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "faraday"
4
+ require "logger"
4
5
 
5
6
  module Exa
6
7
  class Connection
8
+ DEFAULT_BASE_URL = "https://api.exa.ai"
7
9
  def self.build(api_key:, **options, &block)
8
10
  Faraday.new(url: options[:base_url] || DEFAULT_BASE_URL) do |conn|
9
11
  # Authentication
10
- conn.request :authorization, "Bearer", api_key
12
+ conn.headers["x-api-key"] = api_key
11
13
 
12
14
  # Request/Response JSON encoding
13
15
  conn.request :json
14
16
 
17
+ # Debug logging (when enabled via option)
18
+ if options[:debug]
19
+ conn.response :logger, Logger.new($stdout), headers: true, bodies: true
20
+ end
21
+
15
22
  # Custom error handling (registered before JSON so it runs after in response chain)
16
23
  conn.response :raise_error
17
24
  conn.response :json, content_type: /\bjson$/
@@ -0,0 +1,90 @@
1
+ require "json"
2
+
3
+ module Exa
4
+ module Services
5
+ class AnswerStream
6
+ def initialize(connection, **params)
7
+ @connection = connection
8
+ @params = params.merge(stream: true)
9
+ end
10
+
11
+ def call(&block)
12
+ raise ArgumentError, "block required for streaming" unless block_given?
13
+
14
+ # Use instance variable to track buffer across on_data callbacks
15
+ @buffer = ""
16
+
17
+ # Configure the request to stream chunks via on_data callback
18
+ @connection.post("/answer", @params) do |req|
19
+ req.options.on_data = proc do |chunk|
20
+ # Add chunk to buffer and process complete SSE events
21
+ @buffer += chunk
22
+ process_sse_buffer(&block)
23
+ end
24
+ end
25
+
26
+ # Process any remaining data in buffer after stream ends
27
+ process_remaining_buffer(&block) if @buffer.length.positive?
28
+ end
29
+
30
+ private
31
+
32
+ def process_sse_buffer
33
+ # Extract and process complete SSE events (separated by \n\n)
34
+ # If buffer ends with \n\n, all parts are complete.
35
+ # Otherwise, keep the last part in buffer for next chunk.
36
+
37
+ return if @buffer.empty?
38
+
39
+ parts = @buffer.split("\n\n")
40
+
41
+ # When split by a delimiter, if the string ends with the delimiter,
42
+ # split doesn't add a trailing empty string. So we need to track
43
+ # whether the buffer ended with \n\n to know if the last part is incomplete.
44
+ if @buffer.end_with?("\n\n")
45
+ # All parts are complete, clear buffer
46
+ complete_parts = parts
47
+ @buffer = ""
48
+ else
49
+ # Last part is incomplete, keep it for next chunk
50
+ complete_parts = parts[0...-1]
51
+ @buffer = parts.last || ""
52
+ end
53
+
54
+ # Process all complete events
55
+ complete_parts.each do |event|
56
+ next if event.empty?
57
+
58
+ lines = event.split("\n")
59
+ lines.each do |line|
60
+ if line.start_with?("data: ")
61
+ json_str = line.sub(/^data: /, "").strip
62
+ begin
63
+ data = JSON.parse(json_str)
64
+ yield(data)
65
+ rescue JSON::ParserError
66
+ # Skip lines that aren't valid JSON
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ def process_remaining_buffer
74
+ # Process any remaining incomplete buffer
75
+ lines = @buffer.split("\n")
76
+ lines.each do |line|
77
+ if line.start_with?("data: ")
78
+ json_str = line.sub(/^data: /, "").strip
79
+ begin
80
+ data = JSON.parse(json_str)
81
+ yield(data)
82
+ rescue JSON::ParserError
83
+ # Skip lines that aren't valid JSON
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
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.2.0"
4
+ VERSION = "0.3.1"
5
5
  end
data/lib/exa-ai.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Main entry point for the exa-ai gem
4
+ # This file exists to avoid conflicts with the unrelated "exa" gem
5
+ require_relative "exa"