exa-ai-ruby 1.0.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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +4 -0
  3. data/LICENSE +21 -0
  4. data/README.md +247 -0
  5. data/lib/exa/client.rb +33 -0
  6. data/lib/exa/errors.rb +34 -0
  7. data/lib/exa/internal/transport/base_client.rb +171 -0
  8. data/lib/exa/internal/transport/pooled_net_requester.rb +113 -0
  9. data/lib/exa/internal/transport/stream.rb +74 -0
  10. data/lib/exa/internal/util.rb +133 -0
  11. data/lib/exa/resources/base.rb +26 -0
  12. data/lib/exa/resources/events.rb +32 -0
  13. data/lib/exa/resources/imports.rb +58 -0
  14. data/lib/exa/resources/research.rb +50 -0
  15. data/lib/exa/resources/search.rb +44 -0
  16. data/lib/exa/resources/webhooks.rb +67 -0
  17. data/lib/exa/resources/websets/enrichments.rb +57 -0
  18. data/lib/exa/resources/websets/items.rb +40 -0
  19. data/lib/exa/resources/websets/monitors.rb +75 -0
  20. data/lib/exa/resources/websets.rb +71 -0
  21. data/lib/exa/resources.rb +9 -0
  22. data/lib/exa/responses/contents_response.rb +35 -0
  23. data/lib/exa/responses/event_response.rb +43 -0
  24. data/lib/exa/responses/helpers.rb +29 -0
  25. data/lib/exa/responses/import_response.rb +90 -0
  26. data/lib/exa/responses/monitor_response.rb +77 -0
  27. data/lib/exa/responses/raw_response.rb +14 -0
  28. data/lib/exa/responses/research_response.rb +56 -0
  29. data/lib/exa/responses/result.rb +61 -0
  30. data/lib/exa/responses/search_response.rb +43 -0
  31. data/lib/exa/responses/webhook_response.rb +95 -0
  32. data/lib/exa/responses/webset_response.rb +136 -0
  33. data/lib/exa/responses.rb +13 -0
  34. data/lib/exa/types/answer.rb +30 -0
  35. data/lib/exa/types/base.rb +66 -0
  36. data/lib/exa/types/contents.rb +25 -0
  37. data/lib/exa/types/enums.rb +47 -0
  38. data/lib/exa/types/find_similar.rb +26 -0
  39. data/lib/exa/types/research.rb +18 -0
  40. data/lib/exa/types/schema.rb +58 -0
  41. data/lib/exa/types/search.rb +74 -0
  42. data/lib/exa/types.rb +10 -0
  43. data/lib/exa/version.rb +5 -0
  44. data/lib/exa.rb +14 -0
  45. metadata +170 -0
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Exa
6
+ module Internal
7
+ module Transport
8
+ class Stream
9
+ include Enumerable
10
+
11
+ def initialize(headers:, stream:)
12
+ @headers = headers
13
+ @stream = stream
14
+ end
15
+
16
+ def each(&blk)
17
+ return enum_for(__method__) unless block_given?
18
+ @stream.each(&blk)
19
+ end
20
+
21
+ def each_line(&blk)
22
+ return enum_for(__method__) unless block_given?
23
+ enumerator = Exa::Internal::Util.decode_lines(@stream)
24
+ begin
25
+ enumerator.each do |line|
26
+ yield line.chomp
27
+ end
28
+ ensure
29
+ close
30
+ end
31
+ end
32
+
33
+ def each_json_line(symbolize: true, &blk)
34
+ return enum_for(__method__, symbolize: symbolize) unless block_given?
35
+ each_line do |line|
36
+ next if line.strip.empty?
37
+ yield JSON.parse(line, symbolize_names: symbolize)
38
+ end
39
+ end
40
+
41
+ def each_event(&blk)
42
+ return enum_for(__method__) unless block_given?
43
+ sse = Exa::Internal::Util.decode_sse(@stream)
44
+ begin
45
+ sse.each do |event|
46
+ payload = event[:data]
47
+ payload = payload.chomp if payload
48
+ yield(event.merge(data: payload))
49
+ end
50
+ ensure
51
+ close
52
+ end
53
+ end
54
+
55
+ def each_event_json(symbolize: true, &blk)
56
+ return enum_for(__method__, symbolize: symbolize) unless block_given?
57
+ each_event do |event|
58
+ data = event[:data]
59
+ next if data.nil? || data.empty?
60
+ yield event.merge(data: JSON.parse(data, symbolize_names: symbolize))
61
+ end
62
+ end
63
+
64
+ def close
65
+ Exa::Internal::Util.close_fused!(@stream)
66
+ end
67
+
68
+ def content_type
69
+ @headers["content-type"]
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "stringio"
5
+ require "set"
6
+
7
+ module Exa
8
+ module Internal
9
+ module Util
10
+ JSON_CONTENT = %r{application/(json|problem\+json)}i.freeze
11
+ JSONL_CONTENT = %r{application/x-ndjson}i.freeze
12
+
13
+ module_function
14
+
15
+ def normalized_headers(headers)
16
+ headers.each_with_object({}) do |(key, value), acc|
17
+ next if value.nil?
18
+ acc[key.to_s.downcase] = value.to_s
19
+ end
20
+ end
21
+
22
+ def deep_merge_hash(base, extra)
23
+ return base unless extra
24
+ base.merge(extra) do |_k, old_val, new_val|
25
+ if old_val.is_a?(Hash) && new_val.is_a?(Hash)
26
+ deep_merge_hash(old_val, new_val)
27
+ else
28
+ new_val
29
+ end
30
+ end
31
+ end
32
+
33
+ def build_query(query)
34
+ return nil if query.nil? || query.empty?
35
+ URI.encode_www_form(query)
36
+ end
37
+
38
+ def decode_content(headers, stream:)
39
+ case headers["content-type"]
40
+ when JSON_CONTENT
41
+ json = stream.to_a.join
42
+ JSON.parse(json, symbolize_names: true)
43
+ when JSONL_CONTENT
44
+ stream.map { JSON.parse(_1, symbolize_names: true) }
45
+ when /^text\/event-stream/
46
+ decode_sse(stream)
47
+ else
48
+ StringIO.new(stream.to_a.join)
49
+ end
50
+ end
51
+
52
+ def force_charset!(content_type, text:)
53
+ return text unless content_type
54
+ return text if text.encoding == Encoding::UTF_8
55
+ if (match = /charset=([^;]+)/i.match(content_type))
56
+ encoding = Encoding.find(match[1])
57
+ text.force_encoding(encoding)
58
+ end
59
+ text
60
+ rescue ArgumentError
61
+ text
62
+ end
63
+
64
+ def decode_lines(enum)
65
+ Enumerator.new do |y|
66
+ buffer = String.new
67
+ enum.each do |chunk|
68
+ buffer << chunk
69
+ while (idx = buffer.index(/\r?\n/))
70
+ y << buffer.slice!(0..idx)
71
+ end
72
+ end
73
+ y << buffer unless buffer.empty?
74
+ end
75
+ end
76
+
77
+ def decode_sse(enum)
78
+ lines = decode_lines(enum)
79
+ Enumerator.new do |y|
80
+ event = {event: nil, data: String.new, id: nil, retry: nil}
81
+ lines.each do |line|
82
+ stripped = line.strip
83
+ if stripped.empty?
84
+ y << event.dup if event[:data]&.length&.positive?
85
+ event = {event: nil, data: String.new, id: nil, retry: nil}
86
+ next
87
+ end
88
+
89
+ case stripped
90
+ when /^event:(.*)$/
91
+ event[:event] = Regexp.last_match(1).strip
92
+ when /^data:(.*)$/
93
+ event[:data] << Regexp.last_match(1).lstrip << "\n"
94
+ when /^id:(.*)$/
95
+ event[:id] = Regexp.last_match(1).strip
96
+ when /^retry:(\d+)$/
97
+ event[:retry] = Regexp.last_match(1).to_i
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ def fused_enum(enum, &on_close)
104
+ closed = false
105
+ wrapper = Enumerator.new do |y|
106
+ begin
107
+ enum.each { y << _1 }
108
+ ensure
109
+ unless closed
110
+ closed = true
111
+ on_close&.call
112
+ end
113
+ end
114
+ end
115
+
116
+ wrapper.define_singleton_method(:close) do
117
+ unless closed
118
+ closed = true
119
+ on_close&.call
120
+ end
121
+ end
122
+
123
+ wrapper
124
+ end
125
+
126
+ def close_fused!(enum)
127
+ if enum.respond_to?(:close)
128
+ enum.close
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Resources
5
+ class Base
6
+ attr_reader :client
7
+
8
+ def initialize(client:)
9
+ @client = client
10
+ end
11
+
12
+ private
13
+
14
+ def serialize(struct_class, params)
15
+ case params
16
+ when struct_class
17
+ params.to_payload
18
+ when Hash
19
+ struct_class.new(**params).to_payload
20
+ else
21
+ raise ArgumentError, "Expected #{struct_class} or Hash, got #{params.class}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Exa
6
+ module Resources
7
+ class Events < Base
8
+ def list(params = nil)
9
+ client.request(
10
+ method: :get,
11
+ path: events_path,
12
+ query: params,
13
+ response_model: Exa::Responses::EventListResponse
14
+ )
15
+ end
16
+
17
+ def retrieve(event_id)
18
+ client.request(
19
+ method: :get,
20
+ path: events_path(event_id),
21
+ response_model: Exa::Responses::Event
22
+ )
23
+ end
24
+
25
+ private
26
+
27
+ def events_path(*parts)
28
+ ["v0", "events", *parts]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Exa
6
+ module Resources
7
+ class Imports < Base
8
+ def create(params)
9
+ client.request(
10
+ method: :post,
11
+ path: imports_path,
12
+ body: params,
13
+ response_model: Exa::Responses::ImportCreationResponse
14
+ )
15
+ end
16
+
17
+ def list(params = nil)
18
+ client.request(
19
+ method: :get,
20
+ path: imports_path,
21
+ query: params,
22
+ response_model: Exa::Responses::ImportListResponse
23
+ )
24
+ end
25
+
26
+ def retrieve(id)
27
+ client.request(
28
+ method: :get,
29
+ path: imports_path(id),
30
+ response_model: Exa::Responses::Import
31
+ )
32
+ end
33
+
34
+ def update(id, params)
35
+ client.request(
36
+ method: :patch,
37
+ path: imports_path(id),
38
+ body: params,
39
+ response_model: Exa::Responses::Import
40
+ )
41
+ end
42
+
43
+ def delete(id)
44
+ client.request(
45
+ method: :delete,
46
+ path: imports_path(id),
47
+ response_model: Exa::Responses::Import
48
+ )
49
+ end
50
+
51
+ private
52
+
53
+ def imports_path(*parts)
54
+ ["v0", "imports", *parts]
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Exa
6
+ module Resources
7
+ class Research < Base
8
+ def create(params)
9
+ payload = serialize(Exa::Types::ResearchCreateRequest, params)
10
+ client.request(
11
+ method: :post,
12
+ path: "research",
13
+ body: payload,
14
+ response_model: Exa::Responses::Research
15
+ )
16
+ end
17
+
18
+ def list(params = nil)
19
+ client.request(
20
+ method: :get,
21
+ path: "research",
22
+ query: params,
23
+ response_model: Exa::Responses::ResearchListResponse
24
+ )
25
+ end
26
+
27
+ def get(research_id, stream: false, events: nil)
28
+ query = {}
29
+ query[:events] = events unless events.nil?
30
+ query[:stream] = stream ? "true" : nil
31
+ response_model = stream ? nil : Exa::Responses::Research
32
+ client.request(
33
+ method: :get,
34
+ path: ["research", research_id],
35
+ query: query.compact,
36
+ stream: stream,
37
+ response_model: response_model
38
+ )
39
+ end
40
+
41
+ def cancel(research_id)
42
+ client.request(
43
+ method: :post,
44
+ path: ["research", research_id, "cancel"],
45
+ response_model: Exa::Responses::Research
46
+ )
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Exa
6
+ module Resources
7
+ class Search < Base
8
+ def search(params)
9
+ payload = serialize(Exa::Types::SearchRequest, params)
10
+ client.request(method: :post, path: "search", body: payload, response_model: Exa::Responses::SearchResponse)
11
+ end
12
+
13
+ def contents(params)
14
+ payload = serialize(Exa::Types::ContentsRequest, params)
15
+ client.request(method: :post, path: "contents", body: payload, response_model: Exa::Responses::ContentsResponse)
16
+ end
17
+
18
+ def find_similar(params)
19
+ payload = serialize(Exa::Types::FindSimilarRequest, params)
20
+ client.request(method: :post, path: "findSimilar", body: payload, response_model: Exa::Responses::FindSimilarResponse)
21
+ end
22
+
23
+ def answer(params)
24
+ normalized = normalize_nested_struct(params, :search_options, Exa::Types::AnswerSearchOptions)
25
+ payload = serialize(Exa::Types::AnswerRequest, normalized)
26
+ client.request(method: :post, path: "answer", body: payload)
27
+ end
28
+
29
+ private
30
+
31
+ def normalize_nested_struct(params, key, struct_class)
32
+ return params unless params.is_a?(Hash)
33
+
34
+ value = params[key] || params[key.to_s]
35
+ return params if value.nil? || value.is_a?(struct_class)
36
+
37
+ merged = params.dup
38
+ merged[key] = struct_class.new(**value)
39
+ merged.delete(key.to_s)
40
+ merged
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Exa
6
+ module Resources
7
+ class Webhooks < Base
8
+ def create(params)
9
+ client.request(
10
+ method: :post,
11
+ path: webhooks_path,
12
+ body: params,
13
+ response_model: Exa::Responses::Webhook
14
+ )
15
+ end
16
+
17
+ def list(params = nil)
18
+ client.request(
19
+ method: :get,
20
+ path: webhooks_path,
21
+ query: params,
22
+ response_model: Exa::Responses::WebhookListResponse
23
+ )
24
+ end
25
+
26
+ def retrieve(id)
27
+ client.request(
28
+ method: :get,
29
+ path: webhooks_path(id),
30
+ response_model: Exa::Responses::Webhook
31
+ )
32
+ end
33
+
34
+ def update(id, params)
35
+ client.request(
36
+ method: :patch,
37
+ path: webhooks_path(id),
38
+ body: params,
39
+ response_model: Exa::Responses::Webhook
40
+ )
41
+ end
42
+
43
+ def delete(id)
44
+ client.request(
45
+ method: :delete,
46
+ path: webhooks_path(id),
47
+ response_model: Exa::Responses::Webhook
48
+ )
49
+ end
50
+
51
+ def attempts(id, params = nil)
52
+ client.request(
53
+ method: :get,
54
+ path: ["v0", "webhooks", id, "attempts"],
55
+ query: params,
56
+ response_model: Exa::Responses::WebhookAttemptListResponse
57
+ )
58
+ end
59
+
60
+ private
61
+
62
+ def webhooks_path(*parts)
63
+ ["v0", "webhooks", *parts]
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Resources
5
+ class Websets
6
+ class Enrichments < Base
7
+ def create(webset_id, params)
8
+ client.request(
9
+ method: :post,
10
+ path: enrichments_path(webset_id),
11
+ body: params,
12
+ response_model: Exa::Responses::WebsetEnrichment
13
+ )
14
+ end
15
+
16
+ def retrieve(webset_id, enrichment_id)
17
+ client.request(
18
+ method: :get,
19
+ path: enrichments_path(webset_id, enrichment_id),
20
+ response_model: Exa::Responses::WebsetEnrichment
21
+ )
22
+ end
23
+
24
+ def update(webset_id, enrichment_id, params)
25
+ client.request(
26
+ method: :patch,
27
+ path: enrichments_path(webset_id, enrichment_id),
28
+ body: params,
29
+ response_model: Exa::Responses::WebsetEnrichment
30
+ )
31
+ end
32
+
33
+ def delete(webset_id, enrichment_id)
34
+ client.request(
35
+ method: :delete,
36
+ path: enrichments_path(webset_id, enrichment_id),
37
+ response_model: Exa::Responses::WebsetEnrichment
38
+ )
39
+ end
40
+
41
+ def cancel(webset_id, enrichment_id)
42
+ client.request(
43
+ method: :post,
44
+ path: enrichments_path(webset_id, enrichment_id, "cancel"),
45
+ response_model: Exa::Responses::WebsetEnrichment
46
+ )
47
+ end
48
+
49
+ private
50
+
51
+ def enrichments_path(webset_id, *parts)
52
+ ["v0", "websets", webset_id, "enrichments", *parts]
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Resources
5
+ class Websets
6
+ class Items < Base
7
+ def list(webset_id, params = nil)
8
+ client.request(
9
+ method: :get,
10
+ path: items_path(webset_id),
11
+ query: params,
12
+ response_model: Exa::Responses::WebsetItemListResponse
13
+ )
14
+ end
15
+
16
+ def retrieve(webset_id, item_id)
17
+ client.request(
18
+ method: :get,
19
+ path: items_path(webset_id, item_id),
20
+ response_model: Exa::Responses::WebsetItem
21
+ )
22
+ end
23
+
24
+ def delete(webset_id, item_id)
25
+ client.request(
26
+ method: :delete,
27
+ path: items_path(webset_id, item_id),
28
+ response_model: Exa::Responses::WebsetItem
29
+ )
30
+ end
31
+
32
+ private
33
+
34
+ def items_path(webset_id, *parts)
35
+ ["v0", "websets", webset_id, "items", *parts]
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Resources
5
+ class Websets
6
+ class Monitors < Base
7
+ def create(params)
8
+ client.request(
9
+ method: :post,
10
+ path: monitor_path,
11
+ body: params,
12
+ response_model: Exa::Responses::Monitor
13
+ )
14
+ end
15
+
16
+ def list(params = nil)
17
+ client.request(
18
+ method: :get,
19
+ path: monitor_path,
20
+ query: params,
21
+ response_model: Exa::Responses::MonitorListResponse
22
+ )
23
+ end
24
+
25
+ def retrieve(id)
26
+ client.request(
27
+ method: :get,
28
+ path: monitor_path(id),
29
+ response_model: Exa::Responses::Monitor
30
+ )
31
+ end
32
+
33
+ def update(id, params)
34
+ client.request(
35
+ method: :patch,
36
+ path: monitor_path(id),
37
+ body: params,
38
+ response_model: Exa::Responses::Monitor
39
+ )
40
+ end
41
+
42
+ def delete(id)
43
+ client.request(
44
+ method: :delete,
45
+ path: monitor_path(id),
46
+ response_model: Exa::Responses::Monitor
47
+ )
48
+ end
49
+
50
+ def runs_list(monitor_id, params = nil)
51
+ client.request(
52
+ method: :get,
53
+ path: monitor_path(monitor_id, "runs"),
54
+ query: params,
55
+ response_model: Exa::Responses::MonitorRunListResponse
56
+ )
57
+ end
58
+
59
+ def runs_get(monitor_id, run_id)
60
+ client.request(
61
+ method: :get,
62
+ path: monitor_path(monitor_id, "runs", run_id),
63
+ response_model: Exa::Responses::MonitorRun
64
+ )
65
+ end
66
+
67
+ private
68
+
69
+ def monitor_path(*parts)
70
+ ["v0", "monitors", *parts]
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end