exa-ai 0.3.0 → 0.4.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +94 -591
  3. data/exe/exa-ai +112 -9
  4. data/exe/exa-ai-answer +1 -5
  5. data/exe/exa-ai-context +1 -4
  6. data/exe/exa-ai-enrichment-cancel +107 -0
  7. data/exe/exa-ai-enrichment-create +235 -0
  8. data/exe/exa-ai-enrichment-delete +121 -0
  9. data/exe/exa-ai-enrichment-get +103 -0
  10. data/exe/exa-ai-enrichment-list +98 -0
  11. data/exe/exa-ai-enrichment-update +170 -0
  12. data/exe/exa-ai-find-similar +240 -0
  13. data/exe/exa-ai-get-contents +1 -4
  14. data/exe/exa-ai-research-get +1 -2
  15. data/exe/exa-ai-research-list +1 -2
  16. data/exe/exa-ai-research-start +1 -3
  17. data/exe/exa-ai-search +1 -3
  18. data/exe/exa-ai-webset-cancel +96 -0
  19. data/exe/exa-ai-webset-create +192 -0
  20. data/exe/exa-ai-webset-delete +110 -0
  21. data/exe/exa-ai-webset-get +92 -0
  22. data/exe/exa-ai-webset-item-delete +111 -0
  23. data/exe/exa-ai-webset-item-get +104 -0
  24. data/exe/exa-ai-webset-item-list +93 -0
  25. data/exe/exa-ai-webset-list +90 -0
  26. data/exe/exa-ai-webset-search-cancel +103 -0
  27. data/exe/exa-ai-webset-search-create +233 -0
  28. data/exe/exa-ai-webset-search-get +104 -0
  29. data/exe/exa-ai-webset-update +139 -0
  30. data/lib/exa/cli/base.rb +3 -3
  31. data/lib/exa/cli/formatters/enrichment_formatter.rb +69 -0
  32. data/lib/exa/cli/formatters/webset_formatter.rb +68 -0
  33. data/lib/exa/cli/formatters/webset_item_formatter.rb +69 -0
  34. data/lib/exa/client.rb +172 -0
  35. data/lib/exa/connection.rb +8 -1
  36. data/lib/exa/resources/webset.rb +74 -0
  37. data/lib/exa/resources/webset_collection.rb +33 -0
  38. data/lib/exa/resources/webset_enrichment.rb +71 -0
  39. data/lib/exa/resources/webset_enrichment_collection.rb +28 -0
  40. data/lib/exa/resources/webset_search.rb +112 -0
  41. data/lib/exa/services/parameter_converter.rb +1 -0
  42. data/lib/exa/services/websets/cancel.rb +36 -0
  43. data/lib/exa/services/websets/cancel_enrichment.rb +35 -0
  44. data/lib/exa/services/websets/cancel_search.rb +44 -0
  45. data/lib/exa/services/websets/create.rb +45 -0
  46. data/lib/exa/services/websets/create_enrichment.rb +35 -0
  47. data/lib/exa/services/websets/create_search.rb +48 -0
  48. data/lib/exa/services/websets/create_search_validator.rb +128 -0
  49. data/lib/exa/services/websets/create_validator.rb +189 -0
  50. data/lib/exa/services/websets/delete.rb +36 -0
  51. data/lib/exa/services/websets/delete_enrichment.rb +35 -0
  52. data/lib/exa/services/websets/delete_item.rb +20 -0
  53. data/lib/exa/services/websets/get_item.rb +20 -0
  54. data/lib/exa/services/websets/get_search.rb +43 -0
  55. data/lib/exa/services/websets/list.rb +25 -0
  56. data/lib/exa/services/websets/list_items.rb +20 -0
  57. data/lib/exa/services/websets/retrieve.rb +47 -0
  58. data/lib/exa/services/websets/retrieve_enrichment.rb +35 -0
  59. data/lib/exa/services/websets/update.rb +37 -0
  60. data/lib/exa/services/websets/update_enrichment.rb +36 -0
  61. data/lib/exa/services/websets_parameter_converter.rb +45 -0
  62. data/lib/exa/version.rb +1 -1
  63. data/lib/exa-ai.rb +5 -0
  64. data/lib/exa.rb +26 -0
  65. metadata +65 -3
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Resources
5
+ # Represents a webset enrichment from the Exa API
6
+ #
7
+ # An enrichment extracts specific data from web entities in a webset.
8
+ class WebsetEnrichment < Struct.new(
9
+ :id,
10
+ :object,
11
+ :status,
12
+ :webset_id,
13
+ :title,
14
+ :description,
15
+ :format,
16
+ :options,
17
+ :instructions,
18
+ :metadata,
19
+ :created_at,
20
+ :updated_at,
21
+ keyword_init: true
22
+ )
23
+ def initialize(
24
+ id:,
25
+ object:,
26
+ status:,
27
+ webset_id: nil,
28
+ title: nil,
29
+ description: nil,
30
+ format: nil,
31
+ options: nil,
32
+ instructions: nil,
33
+ metadata: nil,
34
+ created_at: nil,
35
+ updated_at: nil
36
+ )
37
+ super
38
+ freeze
39
+ end
40
+
41
+ def pending?
42
+ status == "pending"
43
+ end
44
+
45
+ def running?
46
+ status == "running"
47
+ end
48
+
49
+ def completed?
50
+ status == "completed"
51
+ end
52
+
53
+ def to_h
54
+ {
55
+ id: id,
56
+ object: object,
57
+ status: status,
58
+ webset_id: webset_id,
59
+ title: title,
60
+ description: description,
61
+ format: format,
62
+ options: options,
63
+ instructions: instructions,
64
+ metadata: metadata,
65
+ created_at: created_at,
66
+ updated_at: updated_at
67
+ }.compact
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Resources
5
+ # Represents a list of enrichments for a webset from the Exa API
6
+ #
7
+ # This class wraps the JSON response from the GET /websets/v0/websets/{id}/enrichments endpoint
8
+ class WebsetEnrichmentCollection < Struct.new(
9
+ :data,
10
+ keyword_init: true
11
+ )
12
+ def initialize(data:)
13
+ super
14
+ freeze
15
+ end
16
+
17
+ def empty?
18
+ data.empty?
19
+ end
20
+
21
+ def to_h
22
+ {
23
+ data: data
24
+ }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Resources
5
+ # Represents a search operation within a webset
6
+ #
7
+ # A search finds entities matching specific criteria and can either
8
+ # override existing webset items or append to them.
9
+ class WebsetSearch < Struct.new(
10
+ :id,
11
+ :object,
12
+ :status,
13
+ :webset_id,
14
+ :query,
15
+ :entity,
16
+ :criteria,
17
+ :count,
18
+ :behavior,
19
+ :exclude,
20
+ :scope,
21
+ :progress,
22
+ :recall,
23
+ :metadata,
24
+ :canceled_at,
25
+ :canceled_reason,
26
+ :created_at,
27
+ :updated_at,
28
+ keyword_init: true
29
+ )
30
+ def initialize(
31
+ id:,
32
+ object:,
33
+ status:,
34
+ webset_id: nil,
35
+ query: nil,
36
+ entity: nil,
37
+ criteria: nil,
38
+ count: nil,
39
+ behavior: nil,
40
+ exclude: nil,
41
+ scope: nil,
42
+ progress: nil,
43
+ recall: nil,
44
+ metadata: nil,
45
+ canceled_at: nil,
46
+ canceled_reason: nil,
47
+ created_at: nil,
48
+ updated_at: nil
49
+ )
50
+ super
51
+ freeze
52
+ end
53
+
54
+ # Status helper methods
55
+ def created?
56
+ status == "created"
57
+ end
58
+
59
+ def running?
60
+ status == "running"
61
+ end
62
+
63
+ def completed?
64
+ status == "completed"
65
+ end
66
+
67
+ def failed?
68
+ status == "failed"
69
+ end
70
+
71
+ def canceled?
72
+ status == "canceled"
73
+ end
74
+
75
+ def in_progress?
76
+ created? || running?
77
+ end
78
+
79
+ # Behavior helper methods
80
+ def override?
81
+ behavior == "override"
82
+ end
83
+
84
+ def append?
85
+ behavior == "append"
86
+ end
87
+
88
+ def to_h
89
+ {
90
+ id: id,
91
+ object: object,
92
+ status: status,
93
+ webset_id: webset_id,
94
+ query: query,
95
+ entity: entity,
96
+ criteria: criteria,
97
+ count: count,
98
+ behavior: behavior,
99
+ exclude: exclude,
100
+ scope: scope,
101
+ progress: progress,
102
+ recall: recall,
103
+ metadata: metadata,
104
+ canceled_at: canceled_at,
105
+ canceled_reason: canceled_reason,
106
+ created_at: created_at,
107
+ updated_at: updated_at
108
+ }.compact
109
+ end
110
+ end
111
+ end
112
+ end
@@ -35,6 +35,7 @@ module Exa
35
35
  when :end_crawl_date then :endCrawlDate
36
36
  when :include_text then :includeText
37
37
  when :exclude_text then :excludeText
38
+ when :external_id then :externalId
38
39
  else
39
40
  key
40
41
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Services
5
+ module Websets
6
+ class Cancel
7
+ def initialize(connection, id:)
8
+ @connection = connection
9
+ @id = id
10
+ end
11
+
12
+ def call
13
+ response = @connection.post("/websets/v0/websets/#{@id}/cancel", {})
14
+ body = response.body
15
+
16
+ Resources::Webset.new(
17
+ id: body["id"],
18
+ object: body["object"],
19
+ status: body["status"],
20
+ external_id: body["externalId"],
21
+ title: body["title"],
22
+ searches: body["searches"],
23
+ imports: body["imports"],
24
+ enrichments: body["enrichments"],
25
+ monitors: body["monitors"],
26
+ excludes: body["excludes"],
27
+ metadata: body["metadata"],
28
+ created_at: body["createdAt"],
29
+ updated_at: body["updatedAt"],
30
+ items: body["items"]
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Services
5
+ module Websets
6
+ class CancelEnrichment
7
+ def initialize(connection, webset_id:, id:)
8
+ @connection = connection
9
+ @webset_id = webset_id
10
+ @id = id
11
+ end
12
+
13
+ def call
14
+ response = @connection.post("/websets/v0/websets/#{@webset_id}/enrichments/#{@id}/cancel", {})
15
+ body = response.body
16
+
17
+ Resources::WebsetEnrichment.new(
18
+ id: body["id"],
19
+ object: body["object"],
20
+ status: body["status"],
21
+ webset_id: body["websetId"],
22
+ title: body["title"],
23
+ description: body["description"],
24
+ format: body["format"],
25
+ options: body["options"],
26
+ instructions: body["instructions"],
27
+ metadata: body["metadata"],
28
+ created_at: body["createdAt"],
29
+ updated_at: body["updatedAt"]
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Services
5
+ module Websets
6
+ class CancelSearch
7
+ def initialize(connection, webset_id:, id:)
8
+ @connection = connection
9
+ @webset_id = webset_id
10
+ @id = id
11
+ end
12
+
13
+ def call
14
+ response = @connection.post(
15
+ "/websets/v0/websets/#{@webset_id}/searches/#{@id}/cancel",
16
+ {}
17
+ )
18
+ body = response.body
19
+
20
+ Resources::WebsetSearch.new(
21
+ id: body["id"],
22
+ object: body["object"],
23
+ status: body["status"],
24
+ webset_id: body["websetId"],
25
+ query: body["query"],
26
+ entity: body["entity"],
27
+ criteria: body["criteria"],
28
+ count: body["count"],
29
+ behavior: body["behavior"],
30
+ exclude: body["exclude"],
31
+ scope: body["scope"],
32
+ progress: body["progress"],
33
+ recall: body["recall"],
34
+ metadata: body["metadata"],
35
+ canceled_at: body["canceledAt"],
36
+ canceled_reason: body["canceledReason"],
37
+ created_at: body["createdAt"],
38
+ updated_at: body["updatedAt"]
39
+ )
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "create_validator"
4
+ require_relative "../websets_parameter_converter"
5
+
6
+ module Exa
7
+ module Services
8
+ module Websets
9
+ class Create
10
+ def initialize(connection, **params)
11
+ @connection = connection
12
+ @params = params
13
+ end
14
+
15
+ def call
16
+ # Validate parameters before making the API call
17
+ CreateValidator.validate!(@params)
18
+
19
+ # Convert Ruby snake_case params to API camelCase
20
+ converted_params = WebsetsParameterConverter.convert(@params)
21
+
22
+ response = @connection.post("/websets/v0/websets", converted_params)
23
+ body = response.body
24
+
25
+ Resources::Webset.new(
26
+ id: body["id"],
27
+ object: body["object"],
28
+ status: body["status"],
29
+ external_id: body["externalId"],
30
+ title: body["title"],
31
+ searches: body["searches"],
32
+ imports: body["imports"],
33
+ enrichments: body["enrichments"],
34
+ monitors: body["monitors"],
35
+ excludes: body["excludes"],
36
+ metadata: body["metadata"],
37
+ created_at: body["createdAt"],
38
+ updated_at: body["updatedAt"],
39
+ items: body["items"]
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Services
5
+ module Websets
6
+ class CreateEnrichment
7
+ def initialize(connection, webset_id:, **params)
8
+ @connection = connection
9
+ @webset_id = webset_id
10
+ @params = params
11
+ end
12
+
13
+ def call
14
+ response = @connection.post("/websets/v0/websets/#{@webset_id}/enrichments", @params)
15
+ body = response.body
16
+
17
+ Resources::WebsetEnrichment.new(
18
+ id: body["id"],
19
+ object: body["object"],
20
+ status: body["status"],
21
+ webset_id: body["websetId"],
22
+ title: body["title"],
23
+ description: body["description"],
24
+ format: body["format"],
25
+ options: body["options"],
26
+ instructions: body["instructions"],
27
+ metadata: body["metadata"],
28
+ created_at: body["createdAt"],
29
+ updated_at: body["updatedAt"]
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "create_search_validator"
4
+
5
+ module Exa
6
+ module Services
7
+ module Websets
8
+ class CreateSearch
9
+ def initialize(connection, webset_id:, **params)
10
+ @connection = connection
11
+ @webset_id = webset_id
12
+ @params = params
13
+ end
14
+
15
+ def call
16
+ CreateSearchValidator.validate!(@params)
17
+
18
+ response = @connection.post(
19
+ "/websets/v0/websets/#{@webset_id}/searches",
20
+ @params
21
+ )
22
+ body = response.body
23
+
24
+ Resources::WebsetSearch.new(
25
+ id: body["id"],
26
+ object: body["object"],
27
+ status: body["status"],
28
+ webset_id: body["websetId"],
29
+ query: body["query"],
30
+ entity: body["entity"],
31
+ criteria: body["criteria"],
32
+ count: body["count"],
33
+ behavior: body["behavior"],
34
+ exclude: body["exclude"],
35
+ scope: body["scope"],
36
+ progress: body["progress"],
37
+ recall: body["recall"],
38
+ metadata: body["metadata"],
39
+ canceled_at: body["canceledAt"],
40
+ canceled_reason: body["canceledReason"],
41
+ created_at: body["createdAt"],
42
+ updated_at: body["updatedAt"]
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Services
5
+ module Websets
6
+ # Validates parameters for webset search creation
7
+ class CreateSearchValidator
8
+ VALID_ENTITY_TYPES = %w[company person article research_paper custom].freeze
9
+ VALID_BEHAVIORS = %w[override append].freeze
10
+ VALID_SOURCE_TYPES = %w[import webset].freeze
11
+
12
+ class << self
13
+ def validate!(params)
14
+ validate_query!(params[:query]) if params[:query]
15
+ validate_count!(params[:count]) if params[:count]
16
+ validate_entity!(params[:entity]) if params[:entity]
17
+ validate_criteria!(params[:criteria]) if params[:criteria]
18
+ validate_scope!(params[:scope]) if params[:scope]
19
+ validate_exclude!(params[:exclude]) if params[:exclude]
20
+ validate_behavior!(params[:behavior]) if params[:behavior]
21
+ validate_metadata!(params[:metadata]) if params[:metadata]
22
+ end
23
+
24
+ private
25
+
26
+ def validate_query!(query)
27
+ raise ArgumentError, "query must be a String" unless query.is_a?(String)
28
+ raise ArgumentError, "query cannot be empty" if query.strip.empty?
29
+ raise ArgumentError, "query cannot exceed 5000 characters" if query.length > 5000
30
+ end
31
+
32
+ def validate_count!(count)
33
+ raise ArgumentError, "count must be a positive Integer" unless count.is_a?(Integer) && count > 0
34
+ end
35
+
36
+ def validate_entity!(entity)
37
+ raise ArgumentError, "entity must be a Hash" unless entity.is_a?(Hash)
38
+ raise ArgumentError, "entity[:type] is required" unless entity[:type]
39
+
40
+ type = entity[:type]
41
+ unless VALID_ENTITY_TYPES.include?(type)
42
+ raise ArgumentError, "entity[:type] must be one of: #{VALID_ENTITY_TYPES.join(', ')}"
43
+ end
44
+
45
+ if type == "custom"
46
+ raise ArgumentError, "entity[:description] is required for custom entity type" unless entity[:description]
47
+ validate_string_length!(entity[:description], "entity[:description]", min: 2, max: 200)
48
+ end
49
+ end
50
+
51
+ def validate_criteria!(criteria)
52
+ raise ArgumentError, "criteria must be an Array" unless criteria.is_a?(Array)
53
+ raise ArgumentError, "criteria must have at least 1 item" if criteria.empty?
54
+ raise ArgumentError, "criteria cannot have more than 5 items" if criteria.length > 5
55
+
56
+ criteria.each_with_index do |criterion, index|
57
+ raise ArgumentError, "criteria[#{index}] must be a Hash" unless criterion.is_a?(Hash)
58
+ raise ArgumentError, "criteria[#{index}][:description] is required" unless criterion[:description]
59
+ validate_string_length!(criterion[:description], "criteria[#{index}][:description]", min: 1, max: 1000)
60
+ end
61
+ end
62
+
63
+ def validate_scope!(scope)
64
+ raise ArgumentError, "scope must be an Array" unless scope.is_a?(Array)
65
+
66
+ scope.each_with_index do |item, index|
67
+ validate_source_reference!(item, "scope[#{index}]")
68
+
69
+ next unless item[:relationship]
70
+
71
+ rel = item[:relationship]
72
+ raise ArgumentError, "scope[#{index}][:relationship] must be a Hash" unless rel.is_a?(Hash)
73
+ raise ArgumentError, "scope[#{index}][:relationship][:definition] is required" unless rel[:definition]
74
+ raise ArgumentError, "scope[#{index}][:relationship][:limit] is required" unless rel[:limit]
75
+
76
+ limit = rel[:limit]
77
+ unless limit.is_a?(Integer) && limit >= 1 && limit <= 10
78
+ raise ArgumentError, "scope[#{index}][:relationship][:limit] must be an Integer between 1 and 10"
79
+ end
80
+ end
81
+ end
82
+
83
+ def validate_exclude!(exclude)
84
+ raise ArgumentError, "exclude must be an Array" unless exclude.is_a?(Array)
85
+
86
+ exclude.each_with_index do |item, index|
87
+ validate_source_reference!(item, "exclude[#{index}]")
88
+ end
89
+ end
90
+
91
+ def validate_behavior!(behavior)
92
+ unless VALID_BEHAVIORS.include?(behavior)
93
+ raise ArgumentError, "behavior must be one of: #{VALID_BEHAVIORS.join(', ')}"
94
+ end
95
+ end
96
+
97
+ def validate_metadata!(metadata)
98
+ raise ArgumentError, "metadata must be a Hash" unless metadata.is_a?(Hash)
99
+
100
+ metadata.each do |key, value|
101
+ raise ArgumentError, "metadata values must be Strings" unless value.is_a?(String)
102
+ raise ArgumentError, "metadata value for '#{key}' cannot exceed 1000 characters" if value.length > 1000
103
+ end
104
+ end
105
+
106
+ def validate_source_reference!(item, context)
107
+ raise ArgumentError, "#{context} must be a Hash" unless item.is_a?(Hash)
108
+ raise ArgumentError, "#{context}[:source] is required" unless item[:source]
109
+ raise ArgumentError, "#{context}[:id] is required" unless item[:id]
110
+
111
+ source = item[:source]
112
+ unless VALID_SOURCE_TYPES.include?(source)
113
+ raise ArgumentError, "#{context}[:source] must be one of: #{VALID_SOURCE_TYPES.join(', ')}"
114
+ end
115
+
116
+ raise ArgumentError, "#{context}[:id] must be a non-empty String" unless item[:id].is_a?(String) && !item[:id].empty?
117
+ end
118
+
119
+ def validate_string_length!(value, name, min: nil, max: nil)
120
+ raise ArgumentError, "#{name} must be a String" unless value.is_a?(String)
121
+ raise ArgumentError, "#{name} must be at least #{min} characters" if min && value.length < min
122
+ raise ArgumentError, "#{name} cannot exceed #{max} characters" if max && value.length > max
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end