apologist 1.0.2

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 (137) hide show
  1. checksums.yaml +7 -0
  2. data/.fern/metadata.json +13 -0
  3. data/.fernignore +2 -0
  4. data/.rubocop.yml +108 -0
  5. data/CONTRIBUTING.md +120 -0
  6. data/README.md +169 -0
  7. data/Rakefile +20 -0
  8. data/custom.gemspec.rb +17 -0
  9. data/lib/Apologist/benchmarks/client.rb +160 -0
  10. data/lib/Apologist/benchmarks/types/benchmark_run_request.rb +39 -0
  11. data/lib/Apologist/benchmarks/types/benchmark_run_request_content.rb +16 -0
  12. data/lib/Apologist/benchmarks/types/benchmark_run_request_reasoning_effort.rb +15 -0
  13. data/lib/Apologist/benchmarks/types/benchmark_run_request_verbosity.rb +16 -0
  14. data/lib/Apologist/benchmarks/types/get_benchmark_run_request.rb +13 -0
  15. data/lib/Apologist/benchmarks/types/get_benchmark_run_response.rb +11 -0
  16. data/lib/Apologist/benchmarks/types/list_benchmark_runs_request.rb +33 -0
  17. data/lib/Apologist/benchmarks/types/list_benchmark_runs_response.rb +17 -0
  18. data/lib/Apologist/channels/client.rb +319 -0
  19. data/lib/Apologist/channels/types/get_discord_channel_status_request.rb +11 -0
  20. data/lib/Apologist/channels/types/get_discord_channel_status_response.rb +15 -0
  21. data/lib/Apologist/channels/types/get_instagram_privacy_policy_request.rb +11 -0
  22. data/lib/Apologist/channels/types/receive_discord_interaction_request.rb +17 -0
  23. data/lib/Apologist/channels/types/receive_facebook_message_request.rb +13 -0
  24. data/lib/Apologist/channels/types/receive_telegram_update_request.rb +13 -0
  25. data/lib/Apologist/channels/types/receive_twilio_message_request.rb +15 -0
  26. data/lib/Apologist/channels/types/verify_facebook_webhook_request.rb +17 -0
  27. data/lib/Apologist/channels/types/verify_facebook_webhook_request_hub_mode.rb +13 -0
  28. data/lib/Apologist/chat/client.rb +345 -0
  29. data/lib/Apologist/chat/types/feedback_request.rb +13 -0
  30. data/lib/Apologist/chat/types/flag_request.rb +13 -0
  31. data/lib/Apologist/chat/types/get_chat_completion_request.rb +11 -0
  32. data/lib/Apologist/chat/types/get_chat_completion_response.rb +11 -0
  33. data/lib/Apologist/chat/types/like_request.rb +13 -0
  34. data/lib/Apologist/chat/types/list_chat_completions_request.rb +45 -0
  35. data/lib/Apologist/chat/types/list_chat_completions_response.rb +17 -0
  36. data/lib/Apologist/chat/types/share_request.rb +17 -0
  37. data/lib/Apologist/client.rb +71 -0
  38. data/lib/Apologist/corpus/client.rb +244 -0
  39. data/lib/Apologist/corpus/types/corpus_search_request.rb +17 -0
  40. data/lib/Apologist/corpus/types/corpus_search_request_filters.rb +25 -0
  41. data/lib/Apologist/corpus/types/impression_request.rb +17 -0
  42. data/lib/Apologist/corpus/types/log_corpus_referral_redirect_request.rb +19 -0
  43. data/lib/Apologist/corpus/types/referral_request.rb +17 -0
  44. data/lib/Apologist/corpus/types/search_corpus_response.rb +11 -0
  45. data/lib/Apologist/corpus/types/view_request.rb +17 -0
  46. data/lib/Apologist/ct_as/client.rb +98 -0
  47. data/lib/Apologist/ct_as/types/cta_click_request.rb +13 -0
  48. data/lib/Apologist/ct_as/types/match_ctas_response.rb +11 -0
  49. data/lib/Apologist/environment.rb +7 -0
  50. data/lib/Apologist/errors/api_error.rb +8 -0
  51. data/lib/Apologist/errors/client_error.rb +17 -0
  52. data/lib/Apologist/errors/redirect_error.rb +8 -0
  53. data/lib/Apologist/errors/response_error.rb +42 -0
  54. data/lib/Apologist/errors/server_error.rb +11 -0
  55. data/lib/Apologist/errors/timeout_error.rb +8 -0
  56. data/lib/Apologist/evaluators/client.rb +164 -0
  57. data/lib/Apologist/evaluators/types/evaluate_content_response.rb +11 -0
  58. data/lib/Apologist/evaluators/types/evaluator_request.rb +31 -0
  59. data/lib/Apologist/evaluators/types/evaluator_request_content.rb +15 -0
  60. data/lib/Apologist/evaluators/types/evaluator_request_reasoning_effort.rb +15 -0
  61. data/lib/Apologist/evaluators/types/evaluator_request_verbosity.rb +16 -0
  62. data/lib/Apologist/evaluators/types/get_evaluation_request.rb +13 -0
  63. data/lib/Apologist/evaluators/types/get_evaluation_response.rb +11 -0
  64. data/lib/Apologist/evaluators/types/list_evaluations_request.rb +35 -0
  65. data/lib/Apologist/evaluators/types/list_evaluations_response.rb +17 -0
  66. data/lib/Apologist/internal/errors/constraint_error.rb +10 -0
  67. data/lib/Apologist/internal/errors/type_error.rb +10 -0
  68. data/lib/Apologist/internal/http/base_request.rb +51 -0
  69. data/lib/Apologist/internal/http/raw_client.rb +252 -0
  70. data/lib/Apologist/internal/iterators/cursor_item_iterator.rb +28 -0
  71. data/lib/Apologist/internal/iterators/cursor_page_iterator.rb +63 -0
  72. data/lib/Apologist/internal/iterators/item_iterator.rb +65 -0
  73. data/lib/Apologist/internal/iterators/offset_item_iterator.rb +30 -0
  74. data/lib/Apologist/internal/iterators/offset_page_iterator.rb +103 -0
  75. data/lib/Apologist/internal/json/request.rb +41 -0
  76. data/lib/Apologist/internal/json/serializable.rb +25 -0
  77. data/lib/Apologist/internal/multipart/multipart_encoder.rb +141 -0
  78. data/lib/Apologist/internal/multipart/multipart_form_data.rb +78 -0
  79. data/lib/Apologist/internal/multipart/multipart_form_data_part.rb +51 -0
  80. data/lib/Apologist/internal/multipart/multipart_request.rb +40 -0
  81. data/lib/Apologist/internal/types/array.rb +47 -0
  82. data/lib/Apologist/internal/types/boolean.rb +34 -0
  83. data/lib/Apologist/internal/types/enum.rb +56 -0
  84. data/lib/Apologist/internal/types/hash.rb +36 -0
  85. data/lib/Apologist/internal/types/model/field.rb +38 -0
  86. data/lib/Apologist/internal/types/model.rb +208 -0
  87. data/lib/Apologist/internal/types/type.rb +35 -0
  88. data/lib/Apologist/internal/types/union.rb +161 -0
  89. data/lib/Apologist/internal/types/unknown.rb +15 -0
  90. data/lib/Apologist/internal/types/utils.rb +116 -0
  91. data/lib/Apologist/internal/url_encoded/request.rb +41 -0
  92. data/lib/Apologist/shares/client.rb +52 -0
  93. data/lib/Apologist/shares/types/get_shared_messages_request.rb +11 -0
  94. data/lib/Apologist/shares/types/get_shared_messages_response.rb +11 -0
  95. data/lib/Apologist/types/chat_completion_request.rb +11 -0
  96. data/lib/Apologist/types/chat_completion_request_logprobs.rb +13 -0
  97. data/lib/Apologist/types/chat_completion_request_metadata.rb +29 -0
  98. data/lib/Apologist/types/chat_completion_request_reasoning_effort.rb +13 -0
  99. data/lib/Apologist/types/chat_completion_request_response_format.rb +11 -0
  100. data/lib/Apologist/types/chat_completion_request_response_format_json_schema.rb +17 -0
  101. data/lib/Apologist/types/chat_completion_request_response_format_type.rb +15 -0
  102. data/lib/Apologist/types/chat_completion_request_tool_choice.rb +13 -0
  103. data/lib/Apologist/types/chat_completion_request_tool_choice_zero.rb +13 -0
  104. data/lib/Apologist/types/chat_completion_request_verbosity.rb +14 -0
  105. data/lib/Apologist/types/chat_completion_response.rb +21 -0
  106. data/lib/Apologist/types/chat_completion_response_choices_item.rb +15 -0
  107. data/lib/Apologist/types/chat_completion_response_usage.rb +13 -0
  108. data/lib/Apologist/types/chat_message.rb +11 -0
  109. data/lib/Apologist/types/chat_message_role.rb +13 -0
  110. data/lib/Apologist/types/cta_match_request.rb +11 -0
  111. data/lib/Apologist/types/error.rb +11 -0
  112. data/lib/Apologist/types/success_response.rb +11 -0
  113. data/lib/Apologist/types/tag_ref.rb +11 -0
  114. data/lib/Apologist/types/user.rb +23 -0
  115. data/lib/Apologist/types/user_flag.rb +18 -0
  116. data/lib/Apologist/types/webhook_agent_ref.rb +11 -0
  117. data/lib/Apologist/types/webhook_cta.rb +13 -0
  118. data/lib/Apologist/types/webhook_evaluation.rb +14 -0
  119. data/lib/Apologist/types/webhook_event_info.rb +13 -0
  120. data/lib/Apologist/types/webhook_event_info_key.rb +28 -0
  121. data/lib/Apologist/types/webhook_named_ref.rb +11 -0
  122. data/lib/Apologist/types/webhook_notification_ref.rb +12 -0
  123. data/lib/Apologist/types/webhook_payload.rb +30 -0
  124. data/lib/Apologist/users/client.rb +193 -0
  125. data/lib/Apologist/users/types/get_user_request.rb +11 -0
  126. data/lib/Apologist/users/types/get_user_response.rb +11 -0
  127. data/lib/Apologist/users/types/list_user_flags_request.rb +13 -0
  128. data/lib/Apologist/users/types/list_user_flags_response.rb +17 -0
  129. data/lib/Apologist/users/types/list_users_request.rb +23 -0
  130. data/lib/Apologist/users/types/list_users_response.rb +17 -0
  131. data/lib/Apologist/users/types/update_user_response.rb +11 -0
  132. data/lib/Apologist/users/types/user_update_request.rb +17 -0
  133. data/lib/Apologist/users/types/user_update_request_tags_item.rb +15 -0
  134. data/lib/Apologist/version.rb +5 -0
  135. data/lib/Apologist.rb +131 -0
  136. data/reference.md +2927 -0
  137. metadata +183 -0
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apologist
4
+ module Evaluators
5
+ class Client
6
+ # @param client [Apologist::Internal::Http::RawClient]
7
+ #
8
+ # @return [void]
9
+ def initialize(client:)
10
+ @client = client
11
+ end
12
+
13
+ # Returns a paginated list of evaluations for the evaluator, scoped to the requesting agent.
14
+ #
15
+ # @param request_options [Hash]
16
+ # @param params [Hash]
17
+ # @option request_options [String] :base_url
18
+ # @option request_options [Hash{String => Object}] :additional_headers
19
+ # @option request_options [Hash{String => Object}] :additional_query_parameters
20
+ # @option request_options [Hash{String => Object}] :additional_body_parameters
21
+ # @option request_options [Integer] :timeout_in_seconds
22
+ # @option params [String] :id
23
+ # @option params [Integer, nil] :page
24
+ # @option params [Integer, nil] :per_page
25
+ # @option params [String, nil] :min_timestamp
26
+ # @option params [String, nil] :max_timestamp
27
+ # @option params [String, nil] :min_duration
28
+ # @option params [String, nil] :max_duration
29
+ # @option params [String, nil] :min_score
30
+ # @option params [String, nil] :max_score
31
+ # @option params [String, nil] :passed
32
+ # @option params [String, nil] :benchmark
33
+ # @option params [String, nil] :benchmark_run_id
34
+ # @option params [String, nil] :benchmark_question_id
35
+ #
36
+ # @example
37
+ # client.evaluators.list_evaluations(id: "id")
38
+ #
39
+ # @return [Apologist::Evaluators::Types::ListEvaluationsResponse]
40
+ def list_evaluations(request_options: {}, **params)
41
+ params = Apologist::Internal::Types::Utils.normalize_keys(params)
42
+ query_params = {}
43
+ query_params["page"] = params[:page] if params.key?(:page)
44
+ query_params["per_page"] = params[:per_page] if params.key?(:per_page)
45
+ query_params["min_timestamp"] = params[:min_timestamp] if params.key?(:min_timestamp)
46
+ query_params["max_timestamp"] = params[:max_timestamp] if params.key?(:max_timestamp)
47
+ query_params["min_duration"] = params[:min_duration] if params.key?(:min_duration)
48
+ query_params["max_duration"] = params[:max_duration] if params.key?(:max_duration)
49
+ query_params["min_score"] = params[:min_score] if params.key?(:min_score)
50
+ query_params["max_score"] = params[:max_score] if params.key?(:max_score)
51
+ query_params["passed"] = params[:passed] if params.key?(:passed)
52
+ query_params["benchmark"] = params[:benchmark] if params.key?(:benchmark)
53
+ query_params["benchmark_run_id"] = params[:benchmark_run_id] if params.key?(:benchmark_run_id)
54
+ query_params["benchmark_question_id"] = params[:benchmark_question_id] if params.key?(:benchmark_question_id)
55
+
56
+ request = Apologist::Internal::JSON::Request.new(
57
+ base_url: request_options[:base_url],
58
+ method: "GET",
59
+ path: "evaluators/#{URI.encode_uri_component(params[:id].to_s)}/evaluations",
60
+ query: query_params,
61
+ request_options: request_options
62
+ )
63
+ begin
64
+ response = @client.send(request)
65
+ rescue Net::HTTPRequestTimeout
66
+ raise Apologist::Errors::TimeoutError
67
+ end
68
+ code = response.code.to_i
69
+ if code.between?(200, 299)
70
+ Apologist::Evaluators::Types::ListEvaluationsResponse.load(response.body)
71
+ else
72
+ error_class = Apologist::Errors::ResponseError.subclass_for_code(code)
73
+ raise error_class.new(response.body, code: code)
74
+ end
75
+ end
76
+
77
+ # Runs an evaluation on the provided content using the specified evaluator
78
+ #
79
+ # @param request_options [Hash]
80
+ # @param params [Apologist::Evaluators::Types::EvaluatorRequest]
81
+ # @option request_options [String] :base_url
82
+ # @option request_options [Hash{String => Object}] :additional_headers
83
+ # @option request_options [Hash{String => Object}] :additional_query_parameters
84
+ # @option request_options [Hash{String => Object}] :additional_body_parameters
85
+ # @option request_options [Integer] :timeout_in_seconds
86
+ # @option params [String] :id
87
+ #
88
+ # @example
89
+ # client.evaluators.evaluate_content(
90
+ # id: "id",
91
+ # content: "content"
92
+ # )
93
+ #
94
+ # @return [Apologist::Evaluators::Types::EvaluateContentResponse]
95
+ def evaluate_content(request_options: {}, **params)
96
+ params = Apologist::Internal::Types::Utils.normalize_keys(params)
97
+ request_data = Apologist::Evaluators::Types::EvaluatorRequest.new(params).to_h
98
+ non_body_param_names = %w[id]
99
+ body = request_data.except(*non_body_param_names)
100
+
101
+ request = Apologist::Internal::JSON::Request.new(
102
+ base_url: request_options[:base_url],
103
+ method: "POST",
104
+ path: "evaluators/#{URI.encode_uri_component(params[:id].to_s)}/evaluations",
105
+ body: body,
106
+ request_options: request_options
107
+ )
108
+ begin
109
+ response = @client.send(request)
110
+ rescue Net::HTTPRequestTimeout
111
+ raise Apologist::Errors::TimeoutError
112
+ end
113
+ code = response.code.to_i
114
+ if code.between?(200, 299)
115
+ Apologist::Evaluators::Types::EvaluateContentResponse.load(response.body)
116
+ else
117
+ error_class = Apologist::Errors::ResponseError.subclass_for_code(code)
118
+ raise error_class.new(response.body, code: code)
119
+ end
120
+ end
121
+
122
+ # Returns a single evaluation for the evaluator, scoped to the requesting agent.
123
+ #
124
+ # @param request_options [Hash]
125
+ # @param params [Hash]
126
+ # @option request_options [String] :base_url
127
+ # @option request_options [Hash{String => Object}] :additional_headers
128
+ # @option request_options [Hash{String => Object}] :additional_query_parameters
129
+ # @option request_options [Hash{String => Object}] :additional_body_parameters
130
+ # @option request_options [Integer] :timeout_in_seconds
131
+ # @option params [String] :id
132
+ # @option params [String] :evaluation_id
133
+ #
134
+ # @example
135
+ # client.evaluators.get_evaluation(
136
+ # id: "id",
137
+ # evaluation_id: "evaluationId"
138
+ # )
139
+ #
140
+ # @return [Apologist::Evaluators::Types::GetEvaluationResponse]
141
+ def get_evaluation(request_options: {}, **params)
142
+ params = Apologist::Internal::Types::Utils.normalize_keys(params)
143
+ request = Apologist::Internal::JSON::Request.new(
144
+ base_url: request_options[:base_url],
145
+ method: "GET",
146
+ path: "evaluators/#{URI.encode_uri_component(params[:id].to_s)}/evaluations/#{URI.encode_uri_component(params[:evaluation_id].to_s)}",
147
+ request_options: request_options
148
+ )
149
+ begin
150
+ response = @client.send(request)
151
+ rescue Net::HTTPRequestTimeout
152
+ raise Apologist::Errors::TimeoutError
153
+ end
154
+ code = response.code.to_i
155
+ if code.between?(200, 299)
156
+ Apologist::Evaluators::Types::GetEvaluationResponse.load(response.body)
157
+ else
158
+ error_class = Apologist::Errors::ResponseError.subclass_for_code(code)
159
+ raise error_class.new(response.body, code: code)
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apologist
4
+ module Evaluators
5
+ module Types
6
+ class EvaluateContentResponse < Internal::Types::Model
7
+ field :result, -> { Internal::Types::Hash[String, Object] }, optional: true, nullable: false
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apologist
4
+ module Evaluators
5
+ module Types
6
+ class EvaluatorRequest < Internal::Types::Model
7
+ field :id, -> { String }, optional: false, nullable: false
8
+
9
+ field :frequency_penalty, -> { Integer }, optional: true, nullable: false
10
+
11
+ field :confidence_threshold, -> { Integer }, optional: true, nullable: false
12
+
13
+ field :content, -> { Apologist::Evaluators::Types::EvaluatorRequestContent }, optional: false, nullable: false
14
+
15
+ field :model, -> { String }, optional: true, nullable: false
16
+
17
+ field :presence_penalty, -> { Integer }, optional: true, nullable: false
18
+
19
+ field :reasoning_effort, -> { Apologist::Evaluators::Types::EvaluatorRequestReasoningEffort }, optional: true, nullable: false
20
+
21
+ field :verbosity, -> { Apologist::Evaluators::Types::EvaluatorRequestVerbosity }, optional: true, nullable: false
22
+
23
+ field :temperature, -> { Integer }, optional: true, nullable: false
24
+
25
+ field :top_p, -> { Integer }, optional: true, nullable: false
26
+
27
+ field :variables, -> { Internal::Types::Hash[String, String] }, optional: true, nullable: false
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apologist
4
+ module Evaluators
5
+ module Types
6
+ class EvaluatorRequestContent < Internal::Types::Model
7
+ extend Apologist::Internal::Types::Union
8
+
9
+ member -> { String }
10
+
11
+ member -> { Internal::Types::Array[Object] }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apologist
4
+ module Evaluators
5
+ module Types
6
+ module EvaluatorRequestReasoningEffort
7
+ extend Apologist::Internal::Types::Enum
8
+
9
+ LOW = "low"
10
+ MEDIUM = "medium"
11
+ HIGH = "high"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apologist
4
+ module Evaluators
5
+ module Types
6
+ module EvaluatorRequestVerbosity
7
+ extend Apologist::Internal::Types::Enum
8
+
9
+ MINIMAL = "minimal"
10
+ LOW = "low"
11
+ MEDIUM = "medium"
12
+ HIGH = "high"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apologist
4
+ module Evaluators
5
+ module Types
6
+ class GetEvaluationRequest < Internal::Types::Model
7
+ field :id, -> { String }, optional: false, nullable: false
8
+
9
+ field :evaluation_id, -> { String }, optional: false, nullable: false, api_name: "evaluationId"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apologist
4
+ module Evaluators
5
+ module Types
6
+ class GetEvaluationResponse < Internal::Types::Model
7
+ field :data, -> { Internal::Types::Hash[String, Object] }, optional: true, nullable: false
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apologist
4
+ module Evaluators
5
+ module Types
6
+ class ListEvaluationsRequest < Internal::Types::Model
7
+ field :id, -> { String }, optional: false, nullable: false
8
+
9
+ field :page, -> { Integer }, optional: true, nullable: false
10
+
11
+ field :per_page, -> { Integer }, optional: true, nullable: false
12
+
13
+ field :min_timestamp, -> { String }, optional: true, nullable: false
14
+
15
+ field :max_timestamp, -> { String }, optional: true, nullable: false
16
+
17
+ field :min_duration, -> { String }, optional: true, nullable: false
18
+
19
+ field :max_duration, -> { String }, optional: true, nullable: false
20
+
21
+ field :min_score, -> { String }, optional: true, nullable: false
22
+
23
+ field :max_score, -> { String }, optional: true, nullable: false
24
+
25
+ field :passed, -> { String }, optional: true, nullable: false
26
+
27
+ field :benchmark, -> { String }, optional: true, nullable: false
28
+
29
+ field :benchmark_run_id, -> { String }, optional: true, nullable: false
30
+
31
+ field :benchmark_question_id, -> { String }, optional: true, nullable: false
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apologist
4
+ module Evaluators
5
+ module Types
6
+ class ListEvaluationsResponse < Internal::Types::Model
7
+ field :data, -> { Internal::Types::Array[Internal::Types::Hash[String, Object]] }, optional: true, nullable: false
8
+
9
+ field :total, -> { Integer }, optional: true, nullable: false
10
+
11
+ field :page, -> { Integer }, optional: true, nullable: false
12
+
13
+ field :per_page, -> { Integer }, optional: true, nullable: false
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apologist
4
+ module Internal
5
+ module Errors
6
+ class ConstraintError < StandardError
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apologist
4
+ module Internal
5
+ module Errors
6
+ class TypeError < StandardError
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apologist
4
+ module Internal
5
+ module Http
6
+ # @api private
7
+ class BaseRequest
8
+ attr_reader :base_url, :path, :method, :headers, :query, :request_options
9
+
10
+ # @param base_url [String] The base URL for the request
11
+ # @param path [String] The path for the request
12
+ # @param method [String] The HTTP method for the request (:get, :post, etc.)
13
+ # @param headers [Hash] Additional headers for the request (optional)
14
+ # @param query [Hash] Query parameters for the request (optional)
15
+ # @param request_options [Apologist::RequestOptions, Hash{Symbol=>Object}, nil]
16
+ def initialize(base_url:, path:, method:, headers: {}, query: {}, request_options: {})
17
+ @base_url = base_url
18
+ @path = path
19
+ @method = method
20
+ @headers = headers
21
+ @query = query
22
+ @request_options = request_options
23
+ end
24
+
25
+ # @return [Hash] The query parameters merged with additional query parameters from request options.
26
+ def encode_query
27
+ additional_query = @request_options&.dig(:additional_query_parameters) || @request_options&.dig("additional_query_parameters") || {}
28
+ @query.merge(additional_query)
29
+ end
30
+
31
+ # Child classes should implement:
32
+ # - encode_headers: Returns the encoded HTTP request headers.
33
+ # - encode_body: Returns the encoded HTTP request body.
34
+
35
+ private
36
+
37
+ # Merges additional_headers from request_options into sdk_headers, filtering out
38
+ # any keys that collide with SDK-set or client-protected headers (case-insensitive).
39
+ # @param sdk_headers [Hash] Headers set by the SDK for this request type.
40
+ # @param protected_keys [Array<String>] Additional header keys that must not be overridden.
41
+ # @return [Hash] The merged headers.
42
+ def merge_additional_headers(sdk_headers, protected_keys: [])
43
+ additional_headers = @request_options&.dig(:additional_headers) || @request_options&.dig("additional_headers") || {}
44
+ all_protected = (sdk_headers.keys + protected_keys).to_set { |k| k.to_s.downcase }
45
+ filtered = additional_headers.reject { |key, _| all_protected.include?(key.to_s.downcase) }
46
+ sdk_headers.merge(filtered)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apologist
4
+ module Internal
5
+ module Http
6
+ # @api private
7
+ class RawClient
8
+ # Default HTTP status codes that trigger a retry
9
+ RETRYABLE_STATUSES = [408, 429, 500, 502, 503, 504, 521, 522, 524].freeze
10
+ # Initial delay between retries in seconds
11
+ INITIAL_RETRY_DELAY = 0.5
12
+ # Maximum delay between retries in seconds
13
+ MAX_RETRY_DELAY = 60.0
14
+ # Jitter factor for randomizing retry delays (20%)
15
+ JITTER_FACTOR = 0.2
16
+
17
+ # @return [String] The base URL for requests
18
+ attr_reader :base_url
19
+
20
+ # @param base_url [String] The base url for the request.
21
+ # @param max_retries [Integer] The number of times to retry a failed request, defaults to 2.
22
+ # @param timeout [Float] The timeout for the request, defaults to 60.0 seconds.
23
+ # @param headers [Hash] The headers for the request.
24
+ # @param auth_provider [Object, nil] An optional auth provider responding to
25
+ # `auth_headers`. When present its headers are resolved on every request so
26
+ # token-based schemes (e.g. OAuth) can refresh an expired token mid-session.
27
+ def initialize(base_url:, max_retries: 2, timeout: 60.0, headers: {}, auth_provider: nil)
28
+ @base_url = base_url
29
+ @max_retries = max_retries
30
+ @timeout = timeout
31
+ @auth_provider = auth_provider
32
+ @default_headers = {
33
+ "X-Fern-Language": "Ruby",
34
+ "X-Fern-SDK-Name": "apologist",
35
+ "X-Fern-SDK-Version": "0.0.1"
36
+ }.merge(headers)
37
+ end
38
+
39
+ # @param request [Apologist::Internal::Http::BaseRequest] The HTTP request.
40
+ # @return [HTTP::Response] The HTTP response.
41
+ def send(request)
42
+ url = build_url(request)
43
+ # Resolve auth headers once per request (not per retry) so token-based
44
+ # providers refresh at most once here; static providers are cheap.
45
+ auth_headers = resolve_auth_headers
46
+ attempt = 0
47
+ response = nil
48
+
49
+ loop do
50
+ http_request = build_http_request(
51
+ url:,
52
+ method: request.method,
53
+ headers: request.encode_headers(protected_keys: @default_headers.keys + auth_headers.keys),
54
+ body: request.encode_body,
55
+ auth_headers: auth_headers
56
+ )
57
+
58
+ conn = connect(url)
59
+ conn.open_timeout = @timeout
60
+ conn.read_timeout = @timeout
61
+ conn.write_timeout = @timeout
62
+ conn.continue_timeout = @timeout
63
+
64
+ response = conn.request(http_request)
65
+
66
+ break unless should_retry?(response, attempt)
67
+
68
+ delay = retry_delay(response, attempt)
69
+ sleep(delay)
70
+ attempt += 1
71
+ end
72
+
73
+ response
74
+ end
75
+
76
+ # Determines if a request should be retried based on the response status code.
77
+ # @param response [Net::HTTPResponse] The HTTP response.
78
+ # @param attempt [Integer] The current retry attempt (0-indexed).
79
+ # @return [Boolean] Whether the request should be retried.
80
+ def should_retry?(response, attempt)
81
+ return false if attempt >= @max_retries
82
+
83
+ status = response.code.to_i
84
+ RETRYABLE_STATUSES.include?(status)
85
+ end
86
+
87
+ # Calculates the delay before the next retry attempt using exponential backoff with jitter.
88
+ # Respects Retry-After header if present.
89
+ # @param response [Net::HTTPResponse] The HTTP response.
90
+ # @param attempt [Integer] The current retry attempt (0-indexed).
91
+ # @return [Float] The delay in seconds before the next retry.
92
+ def retry_delay(response, attempt)
93
+ # Check for Retry-After header (can be seconds or HTTP date)
94
+ retry_after = response["Retry-After"]
95
+ if retry_after
96
+ delay = parse_retry_after(retry_after)
97
+ return [delay, MAX_RETRY_DELAY].min if delay&.positive?
98
+ end
99
+
100
+ # Exponential backoff with jitter: base_delay * 2^attempt
101
+ base_delay = INITIAL_RETRY_DELAY * (2**attempt)
102
+ add_jitter([base_delay, MAX_RETRY_DELAY].min)
103
+ end
104
+
105
+ # Parses the Retry-After header value.
106
+ # @param value [String] The Retry-After header value (seconds or HTTP date).
107
+ # @return [Float, nil] The delay in seconds, or nil if parsing fails.
108
+ def parse_retry_after(value)
109
+ # Try parsing as integer (seconds)
110
+ seconds = Integer(value, exception: false)
111
+ return seconds.to_f if seconds
112
+
113
+ # Try parsing as HTTP date
114
+ begin
115
+ retry_time = Time.httpdate(value)
116
+ delay = retry_time - Time.now
117
+ delay.positive? ? delay : nil
118
+ rescue ArgumentError
119
+ nil
120
+ end
121
+ end
122
+
123
+ # Adds random jitter to a delay value.
124
+ # @param delay [Float] The base delay in seconds.
125
+ # @return [Float] The delay with jitter applied.
126
+ def add_jitter(delay)
127
+ jitter = delay * JITTER_FACTOR * (rand - 0.5) * 2
128
+ [delay + jitter, 0].max
129
+ end
130
+
131
+ LOCALHOST_HOSTS = %w[localhost 127.0.0.1 [::1]].freeze
132
+
133
+ # @param request [Apologist::Internal::Http::BaseRequest] The HTTP request.
134
+ # @return [URI::Generic] The URL.
135
+ def build_url(request)
136
+ encoded_query = request.encode_query
137
+
138
+ # If the path is already an absolute URL, use it directly
139
+ if request.path.start_with?("http://", "https://")
140
+ url = request.path
141
+ url = "#{url}?#{encode_query(encoded_query)}" if encoded_query&.any?
142
+ parsed = URI.parse(url)
143
+ validate_https!(parsed)
144
+ return parsed
145
+ end
146
+
147
+ path = request.path.start_with?("/") ? request.path[1..] : request.path
148
+ base = request.base_url || @base_url
149
+ url = "#{base.chomp("/")}/#{path}"
150
+ url = "#{url}?#{encode_query(encoded_query)}" if encoded_query&.any?
151
+ parsed = URI.parse(url)
152
+ validate_https!(parsed)
153
+ parsed
154
+ end
155
+
156
+ # Raises if the URL uses http:// for a non-localhost host, which would
157
+ # send authentication credentials in plaintext.
158
+ # @param url [URI::Generic] The parsed URL.
159
+ def validate_https!(url)
160
+ return if url.scheme != "http"
161
+ return if LOCALHOST_HOSTS.include?(url.host)
162
+
163
+ raise ArgumentError,
164
+ "Refusing to send request to non-HTTPS URL: #{url}. " \
165
+ "HTTP is only allowed for localhost. Use HTTPS or pass a localhost URL."
166
+ end
167
+
168
+ # Resolves the auth headers to send with the next request. Delegates to the
169
+ # configured auth provider (if any) on every call so that token-based
170
+ # providers (e.g. OAuth client-credentials) can refresh an expired token
171
+ # before the request is sent. Returns an empty hash when no provider is set,
172
+ # which keeps the api-key / basic / bearer / no-auth paths unchanged.
173
+ # @return [Hash] The auth headers for the current request.
174
+ def resolve_auth_headers
175
+ return {} if @auth_provider.nil?
176
+
177
+ @auth_provider.auth_headers
178
+ end
179
+
180
+ # @param url [URI::Generic] The url to the resource.
181
+ # @param method [String] The HTTP method to use.
182
+ # @param headers [Hash] The headers for the request.
183
+ # @param body [String, nil] The body for the request.
184
+ # @param auth_headers [Hash] The auth headers resolved for this request. These
185
+ # take precedence over the static default headers but not over per-request
186
+ # headers, mirroring the previous baked-header precedence.
187
+ # @return [HTTP::Request] The HTTP request.
188
+ def build_http_request(url:, method:, headers: {}, body: nil, auth_headers: {})
189
+ request = Net::HTTPGenericRequest.new(
190
+ method,
191
+ !body.nil?,
192
+ method != "HEAD",
193
+ url
194
+ )
195
+
196
+ request_headers = @default_headers.merge(auth_headers).merge(headers)
197
+ request_headers.each { |name, value| request[name] = value }
198
+ request.body = body if body
199
+
200
+ # Net::HTTP disables its transparent gzip/deflate decoding as soon as an
201
+ # Accept-Encoding header is set explicitly on the request. Re-enable it so
202
+ # that compressed response bodies are still inflated.
203
+ request.extend(DecodeContent) if request_headers.keys.any? { |name| name.to_s.casecmp("accept-encoding").zero? }
204
+
205
+ request
206
+ end
207
+
208
+ # Keeps Net::HTTP's transparent gzip/deflate response decoding enabled
209
+ # even when an Accept-Encoding header is set explicitly on the request.
210
+ # @api private
211
+ module DecodeContent
212
+ def decode_content # rubocop:disable Naming/PredicateMethod
213
+ true
214
+ end
215
+ end
216
+
217
+ # @param query [Hash] The query for the request.
218
+ # @return [String, nil] The encoded query.
219
+ def encode_query(query)
220
+ query.to_h.empty? ? nil : URI.encode_www_form(query)
221
+ end
222
+
223
+ # @param url [URI::Generic] The url to connect to.
224
+ # @return [Net::HTTP] The HTTP connection.
225
+ def connect(url)
226
+ is_https = (url.scheme == "https")
227
+
228
+ port = if url.port
229
+ url.port
230
+ elsif is_https
231
+ Net::HTTP.https_default_port
232
+ else
233
+ Net::HTTP.http_default_port
234
+ end
235
+
236
+ http = Net::HTTP.new(url.host, port)
237
+ http.use_ssl = is_https
238
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER if is_https
239
+ # NOTE: We handle retries at the application level with HTTP status code awareness,
240
+ # so we set max_retries to 0 to disable Net::HTTP's built-in network-level retries.
241
+ http.max_retries = 0
242
+ http
243
+ end
244
+
245
+ # @return [String]
246
+ def inspect
247
+ "#<#{self.class.name}:0x#{object_id.to_s(16)} @base_url=#{@base_url.inspect}>"
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end