rager 0.5.0 → 0.7.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 +49 -7
  3. data/lib/rager/chat/message.rb +10 -0
  4. data/lib/rager/chat/message_content.rb +7 -0
  5. data/lib/rager/chat/message_delta.rb +7 -0
  6. data/lib/rager/chat/options.rb +12 -9
  7. data/lib/rager/chat/providers/openai.rb +64 -48
  8. data/lib/rager/chat/schema.rb +3 -4
  9. data/lib/rager/config.rb +16 -20
  10. data/lib/rager/context.rb +326 -97
  11. data/lib/rager/context_options.rb +23 -0
  12. data/lib/rager/embed/options.rb +4 -3
  13. data/lib/rager/embed/providers/openai.rb +14 -6
  14. data/lib/rager/errors/credentials_error.rb +24 -0
  15. data/lib/rager/errors/dependency_error.rb +23 -0
  16. data/lib/rager/errors/http_error.rb +12 -5
  17. data/lib/rager/errors/options_error.rb +10 -5
  18. data/lib/rager/errors/parse_error.rb +9 -5
  19. data/lib/rager/errors/template_error.rb +10 -4
  20. data/lib/rager/errors/timeout_error.rb +25 -0
  21. data/lib/rager/http/adapters/async_http.rb +66 -14
  22. data/lib/rager/http/adapters/mock.rb +41 -45
  23. data/lib/rager/http/adapters/net_http.rb +144 -0
  24. data/lib/rager/http/request.rb +2 -0
  25. data/lib/rager/{image_gen → image}/options.rb +5 -4
  26. data/lib/rager/{image_gen → image}/output_format.rb +1 -1
  27. data/lib/rager/{image_gen → image}/providers/abstract.rb +4 -4
  28. data/lib/rager/{image_gen → image}/providers/replicate.rb +19 -14
  29. data/lib/rager/{logger.rb → log_strategy.rb} +2 -1
  30. data/lib/rager/{mesh_gen → mesh}/options.rb +4 -3
  31. data/lib/rager/{mesh_gen → mesh}/providers/abstract.rb +4 -4
  32. data/lib/rager/{mesh_gen → mesh}/providers/replicate.rb +20 -14
  33. data/lib/rager/operation.rb +2 -2
  34. data/lib/rager/options.rb +1 -1
  35. data/lib/rager/outcome.rb +25 -0
  36. data/lib/rager/providers.rb +61 -0
  37. data/lib/rager/rerank/{query.rb → input.rb} +8 -1
  38. data/lib/rager/rerank/options.rb +3 -2
  39. data/lib/rager/rerank/providers/abstract.rb +2 -2
  40. data/lib/rager/rerank/providers/cohere.rb +24 -15
  41. data/lib/rager/rerank/result.rb +8 -1
  42. data/lib/rager/result.rb +98 -108
  43. data/lib/rager/search/options.rb +4 -1
  44. data/lib/rager/search/providers/jina.rb +68 -0
  45. data/lib/rager/search/result.rb +9 -2
  46. data/lib/rager/template/input.rb +9 -0
  47. data/lib/rager/template/options.rb +1 -1
  48. data/lib/rager/template/providers/erb.rb +3 -3
  49. data/lib/rager/template/providers/mustache.rb +30 -0
  50. data/lib/rager/types.rb +28 -17
  51. data/lib/rager/utils/http.rb +92 -27
  52. data/lib/rager/utils/replicate.rb +40 -21
  53. data/lib/rager/utils/runtime.rb +21 -0
  54. data/lib/rager/version.rb +1 -1
  55. metadata +22 -36
  56. data/lib/rager/chat.rb +0 -35
  57. data/lib/rager/embed.rb +0 -35
  58. data/lib/rager/errors/missing_credentials_error.rb +0 -19
  59. data/lib/rager/errors/unknown_provider_error.rb +0 -17
  60. data/lib/rager/image_gen.rb +0 -31
  61. data/lib/rager/mesh_gen.rb +0 -31
  62. data/lib/rager/rerank.rb +0 -35
  63. data/lib/rager/search/providers/brave.rb +0 -59
  64. data/lib/rager/search.rb +0 -35
  65. data/lib/rager/template.rb +0 -35
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34c1c60d29be37b8a1f9ebb76d975257b26d89aa0345f3bdfb58290e61c82971
4
- data.tar.gz: 47d3f41d9b7f9b08701c2b4b82e15cf7f86a7e3e4ffe9dced6e343ea28304597
3
+ metadata.gz: a240f204b0677aee45009f9357cea5e13b85eaa77dd9696db6e12e065f0144b4
4
+ data.tar.gz: 8d31692e3d8fe5d962ff4f2a9a94817a8886dd20f01eb72cbb24189deb490365
5
5
  SHA512:
6
- metadata.gz: 6dfa77b1500bc0a2693141bff0c6551b72448d100f53f0e612b572d3b894ad5acc09f2cf3bb727d12141511bf902d28b3d43b2e359afc5d3fc9f172aae7caa06
7
- data.tar.gz: d9522f23cd419a3b747f5e97e6097fc751008aedc3111954194de0253a24813a1919e0c5720cb653e51407e41173d80080461dadadb4590b716f638ea755444f
6
+ metadata.gz: 381d1ee7c8202be21536d209d3845039223709e7dd4e93c974f1970ce4b019f8538cac0bcb6cae7c8c15209e86bde9d6adb0e10d9e9b4546734744d4be7133dd
7
+ data.tar.gz: 34341e107e1018810723d9bdbd5f6e0b6fc69f66daf166860aec9f4513b29827479754ba84afe07cf5e9ece45cacf23c0e34f70f8fe896fd14c2b99d86a9f878
data/README.md CHANGED
@@ -4,23 +4,65 @@
4
4
  [![publish](https://github.com/mvkvc/rager_rb/actions/workflows/publish.yml/badge.svg?branch=main)](https://github.com/mvkvc/rager_rb/actions/workflows/publish.yml)
5
5
  [![test](https://github.com/mvkvc/rager_rb/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/mvkvc/rager_rb/actions/workflows/test.yml)
6
6
  [![lint](https://github.com/mvkvc/rager_rb/actions/workflows/lint.yml/badge.svg?branch=main)](https://github.com/mvkvc/rager_rb/actions/workflows/lint.yml)
7
+ [![docs](https://github.com/mvkvc/rager_rb/actions/workflows/docs.yml/badge.svg?branch=main)](https://github.com/mvkvc/rager_rb/actions/workflows/docs.yml)
7
8
 
8
- Build continuously improving AI applications.
9
+ Build continuously improving generative workflows.
10
+
11
+ Examples are available in the [`examples/`](./examples/) folder.
9
12
 
10
13
  ## Installation
11
14
 
12
- If you are using Bundler:
15
+ Add this line to your application's Gemfile:
13
16
 
14
- ```bash
15
- bundle add rager
17
+ ```ruby
18
+ gem "rager", "~> 0.7.0"
16
19
  ```
17
20
 
18
- Otherwise you can add it to your Gemfile directly:
21
+ Or use it in a standalone script (requires OPENAI_API_KEY set):
22
+
23
+ ```ruby
24
+ require "bundler/inline"
25
+
26
+ gemfile do
27
+ source "https://rubygems.org"
28
+ gem "async-http", "~> 0.88.0"
29
+ gem "rager", "~> 0.7.0"
30
+ end
31
+
32
+ require "rager"
33
+
34
+ Rager.configure do |config|
35
+ config.http_adapter = Rager::Http::Adapters::AsyncHttp.new
36
+ end
37
+
38
+ PROMPT = ARGV[0] || "Ruby programming"
19
39
 
20
- ```Ruby
21
- gem "rager", "~> 0.4.0"
40
+ Async do
41
+ ctx = Rager::Context.new
42
+ prompt = ctx.template("Tell me about the history of <%= topic %>",{topic: PROMPT})
43
+ chat = ctx.chat(prompt, stream: true)
44
+ chat.stream.each { |d| print d.content }
45
+ end
22
46
  ```
23
47
 
48
+ ## Providers
49
+
50
+ Modalities that take a URL parameter (chat, embedding, rerank) support compatible services, allowing you to use alternative providers that support the same interface.
51
+
52
+ | Feature | Providers |
53
+ | ---------------------- | ---------------- |
54
+ | **Chat** | `openai` |
55
+ | **Embedding** | `openai` |
56
+ | **Image generation** | `replicate` |
57
+ | **3D mesh generation** | `replicate` |
58
+ | **Reranking** | `cohere` |
59
+ | **Search** | `jina` |
60
+ | **Templating** | `erb`,`mustache` |
61
+
62
+ ## Logging
63
+
64
+ The main reason for creating yet another library is to have out-of-the-box logging for workflows and outcomes. The logged data can then be used for few-shot prompting and fine-tuning. This logging server is being developed at [`rager.cloud`](https://rager.cloud) and is closed while I work on it. Contact me if you would like access.
65
+
24
66
  ## License
25
67
 
26
68
  [MIT](./LICENSE.md)
@@ -6,8 +6,18 @@ require "sorbet-runtime"
6
6
  module Rager
7
7
  module Chat
8
8
  class Message < T::Struct
9
+ extend T::Sig
10
+
9
11
  const :role, MessageRole
10
12
  const :content, T.any(String, T::Array[MessageContent])
13
+
14
+ sig { params(options: T.untyped).returns(String) }
15
+ def to_json(options = nil)
16
+ {
17
+ role: role,
18
+ content: content
19
+ }.to_json(options)
20
+ end
11
21
  end
12
22
  end
13
23
  end
@@ -6,9 +6,16 @@ require "sorbet-runtime"
6
6
  module Rager
7
7
  module Chat
8
8
  class MessageContent < T::Struct
9
+ extend T::Sig
10
+
9
11
  const :type, MessageContentType, default: MessageContentType::Text
10
12
  const :image_type, T.nilable(MessageContentImageType)
11
13
  const :content, String
14
+
15
+ sig { params(options: T.untyped).returns(String) }
16
+ def to_json(options = nil)
17
+ serialize.to_json(options)
18
+ end
12
19
  end
13
20
  end
14
21
  end
@@ -6,8 +6,15 @@ require "sorbet-runtime"
6
6
  module Rager
7
7
  module Chat
8
8
  class MessageDelta < T::Struct
9
+ extend T::Sig
10
+
9
11
  const :index, Integer, default: 0
10
12
  const :content, String
13
+
14
+ sig { params(options: T.untyped).returns(String) }
15
+ def to_json(options = nil)
16
+ serialize.to_json(options)
17
+ end
11
18
  end
12
19
  end
13
20
  end
@@ -10,20 +10,21 @@ module Rager
10
10
  extend T::Sig
11
11
  include Rager::Options
12
12
 
13
- const :provider, String, default: "openai"
14
- const :history, T::Array[Message], default: []
15
- const :url, T.nilable(String)
16
- const :api_key, T.nilable(String)
13
+ const :provider, Symbol, default: :openai
17
14
  const :model, T.nilable(String)
18
15
  const :stream, T.nilable(T::Boolean)
19
16
  const :n, T.nilable(Integer)
20
17
  const :temperature, T.nilable(Float)
18
+ const :max_tokens, T.nilable(Integer)
21
19
  const :system_prompt, T.nilable(String)
22
20
  const :schema, T.nilable(Dry::Schema::JSON)
23
21
  const :schema_name, T.nilable(String)
24
22
  const :seed, T.nilable(Integer)
23
+ const :url, T.nilable(String)
24
+ const :api_key, T.nilable(String)
25
+ const :timeout, T.nilable(Numeric)
25
26
 
26
- sig { override.returns(T::Hash[String, T.untyped]) }
27
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
27
28
  def serialize_safe
28
29
  result = serialize
29
30
  result["api_key"] = "[REDACTED]" if result.key?("api_key")
@@ -35,15 +36,17 @@ module Rager
35
36
  def validate
36
37
  if stream && schema
37
38
  raise Rager::Errors::OptionsError.new(
38
- invalid_keys: %w[stream schema],
39
- description: "You cannot use streaming with structured outputs"
39
+ self,
40
+ ["stream", "schema"],
41
+ details: "You cannot use streaming with structured outputs"
40
42
  )
41
43
  end
42
44
 
43
45
  if schema && schema_name.nil?
44
46
  raise Rager::Errors::OptionsError.new(
45
- invalid_keys: %w[schema schema_name],
46
- description: "You must provide a schema name when using structured outputs"
47
+ self,
48
+ ["schema", "schema_name"],
49
+ details: "You must provide a schema name when using structured outputs"
47
50
  )
48
51
  end
49
52
  end
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "json"
5
+
5
6
  require "sorbet-runtime"
6
7
 
7
8
  module Rager
@@ -10,7 +11,8 @@ module Rager
10
11
  class Openai < Rager::Chat::Providers::Abstract
11
12
  extend T::Sig
12
13
 
13
- OpenaiMessages = T.type_alias { T::Array[T::Hash[String, T.any(String, T::Array[T.untyped])]] }
14
+ OpenaiContentItem = T.type_alias { T::Hash[String, T.any(String, T::Hash[String, String])] }
15
+ OpenaiMessages = T.type_alias { T::Array[T::Hash[String, T.any(String, T::Array[OpenaiContentItem])]] }
14
16
 
15
17
  sig do
16
18
  override.params(
@@ -20,14 +22,24 @@ module Rager
20
22
  end
21
23
  def chat(messages, options)
22
24
  api_key = options.api_key || ENV["OPENAI_API_KEY"]
23
- raise Rager::Errors::MissingCredentialsError.new("OpenAI", "OPENAI_API_KEY") if api_key.nil?
25
+ raise Rager::Errors::CredentialsError.new("OpenAI", env_var: ["OPENAI_API_KEY"]) if api_key.nil?
26
+
27
+ base_url = options.url || ENV["OPENAI_URL"] || "https://api.openai.com/v1"
28
+ url = "#{base_url}/chat/completions"
29
+
30
+ headers = {
31
+ "Content-Type" => "application/json"
32
+ }.tap do |h|
33
+ h["Authorization"] = "Bearer #{api_key}" if api_key
34
+ end
24
35
 
25
36
  body = {
26
37
  model: options.model || "gpt-4.1",
27
- messages: build_openai_messages(messages, options.history, options.system_prompt)
38
+ messages: build_openai_messages(messages, options.system_prompt)
28
39
  }.tap do |b|
29
- b[:temperature] = options.temperature if options.temperature
30
40
  b[:n] = options.n if options.n
41
+ b[:max_tokens] = options.max_tokens if options.max_tokens
42
+ b[:temperature] = options.temperature if options.temperature
31
43
  b[:stream] = options.stream if options.stream
32
44
  b[:seed] = options.seed if options.seed
33
45
 
@@ -43,20 +55,19 @@ module Rager
43
55
  end
44
56
  end
45
57
 
46
- headers = {"Content-Type" => "application/json"}
47
- headers["Authorization"] = "Bearer #{api_key}" if api_key
48
-
49
58
  request = Rager::Http::Request.new(
50
59
  verb: Rager::Http::Verb::Post,
51
- url: options.url || ENV["OPENAI_URL"] || "https://api.openai.com/v1/chat/completions",
60
+ url: url,
52
61
  headers: headers,
53
- body: body.to_json
62
+ body: body.to_json,
63
+ streaming: options.stream || false,
64
+ timeout: options.timeout || Rager.config.timeout
54
65
  )
55
66
 
56
67
  response = Rager.config.http_adapter.make_request(request)
57
68
  response_body = T.must(response.body)
58
69
 
59
- raise Rager::Errors::HttpError.new(Rager.config.http_adapter, response.status, T.cast(response_body, String)) if response.status != 200
70
+ raise Rager::Errors::HttpError.new(Rager.config.http_adapter, request.url, response.status, body: T.cast(response_body, String)) if response.status != 200
60
71
 
61
72
  case response_body
62
73
  when String then handle_non_stream_body(response_body)
@@ -64,39 +75,36 @@ module Rager
64
75
  end
65
76
  end
66
77
 
67
- private
68
-
69
78
  sig do
70
79
  params(
71
80
  messages: T::Array[Rager::Chat::Message],
72
- history: T::Array[Rager::Chat::Message],
73
81
  system_prompt: T.nilable(String)
74
82
  ).returns(OpenaiMessages)
75
83
  end
76
- def build_openai_messages(messages, history, system_prompt)
77
- result = T.let([], OpenaiMessages)
84
+ def build_openai_messages(messages, system_prompt)
85
+ output = T.let([], OpenaiMessages)
78
86
 
79
- if history.empty? && system_prompt && !system_prompt.empty?
80
- result << {"role" => "system", "content" => system_prompt}
87
+ if system_prompt
88
+ output << {"role" => "system", "content" => system_prompt}
81
89
  end
82
90
 
83
- (history + messages).each do |message|
91
+ messages.each do |message|
84
92
  role = message.role.is_a?(String) ? message.role : message.role.serialize
85
93
  content = message.content
86
94
 
87
95
  case content
88
96
  when String
89
- result << {"role" => role, "content" => content}
97
+ output << {"role" => role, "content" => content}
90
98
  when Array
91
99
  formatted_content = content.map { |item| format_content_item(item) }
92
- result << {"role" => role, "content" => formatted_content}
100
+ output << {"role" => role, "content" => formatted_content}
93
101
  end
94
102
  end
95
103
 
96
- result
104
+ output
97
105
  end
98
106
 
99
- sig { params(item: Rager::Chat::MessageContent).returns(T::Hash[String, T.untyped]) }
107
+ sig { params(item: Rager::Chat::MessageContent).returns(OpenaiContentItem) }
100
108
  def format_content_item(item)
101
109
  case item.type
102
110
  when Rager::Chat::MessageContentType::Text
@@ -113,48 +121,56 @@ module Rager
113
121
  end
114
122
  end
115
123
 
116
- sig { params(body: String).returns(T::Array[String]) }
124
+ sig { params(body: String).returns(Rager::Types::ChatNonStreamOutput) }
117
125
  def handle_non_stream_body(body)
118
126
  response_data = JSON.parse(body)
119
127
  return [] unless response_data.key?("choices") && response_data["choices"].is_a?(Array)
120
128
 
121
- response_data["choices"].filter_map do |choice|
129
+ result = response_data["choices"].filter_map do |choice|
122
130
  text = choice.dig("message", "content").to_s
123
131
  text unless text.empty?
124
132
  end
133
+
134
+ result.one? ? result.first : result
125
135
  rescue JSON::ParserError
126
- raise Rager::Errors::ParseError.new("OpenAI response body is not valid JSON", body)
136
+ raise Rager::Errors::ParseError.new(body, details: "OpenAI response body is not valid JSON")
127
137
  end
128
138
 
129
139
  sig { params(body: T::Enumerator[String]).returns(T::Enumerator[Rager::Chat::MessageDelta]) }
130
140
  def handle_stream_body(body)
131
141
  Enumerator.new do |yielder|
132
- buffer = +""
133
-
134
- process_chunk = ->(chunk) do
135
- buffer << chunk
136
- while (match = buffer.match(/\Adata: (.*?)\n\n|\Adata: (.*?)\n/))
137
- buffer.delete_prefix!(T.must(match[0]))
138
- data_line = match[1] || match[2]
139
-
140
- next if data_line.nil? || data_line.strip.empty? || data_line.strip == "[DONE]"
141
-
142
- begin
143
- data = JSON.parse(data_line)
144
- next unless data.key?("choices") && data["choices"].is_a?(Array)
145
-
146
- data["choices"].each do |choice|
147
- delta = choice.dig("delta", "content")
148
- yielder << Rager::Chat::MessageDelta.new(index: choice.dig("index") || 0, content: delta) if delta
149
- end
150
- rescue JSON::ParserError
151
- next
152
- end
142
+ buffer_parts = []
143
+ body.each do |chunk|
144
+ buffer_parts << chunk.force_encoding(Encoding::UTF_8)
145
+ buffer = buffer_parts.join
146
+ while (line = buffer.slice!(/.*\n/))
147
+ process_stream_line(line, yielder)
153
148
  end
149
+ buffer_parts = buffer.empty? ? [] : [buffer]
154
150
  end
155
151
 
156
- body.each(&process_chunk)
157
- process_chunk.call("\n") unless buffer.empty?
152
+ unless buffer_parts.empty?
153
+ process_stream_line(buffer_parts.join, yielder)
154
+ end
155
+ end
156
+ end
157
+
158
+ private
159
+
160
+ sig { params(line: String, yielder: Enumerator::Yielder).void.checked(:never) }
161
+ def process_stream_line(line, yielder)
162
+ line.delete_prefix!("data: ") or return
163
+ line.strip!
164
+ return if line.empty? || line == "[DONE]"
165
+
166
+ begin
167
+ JSON.parse(line).dig("choices")&.each do |choice|
168
+ if (content = choice.dig("delta", "content"))
169
+ yielder << Rager::Chat::MessageDelta.new(index: choice["index"] || 0, content: content)
170
+ end
171
+ end
172
+ rescue JSON::ParserError
173
+ nil
158
174
  end
159
175
  end
160
176
  end
@@ -1,8 +1,9 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "dry-schema"
5
4
  require "json"
5
+
6
+ require "dry-schema"
6
7
  require "sorbet-runtime"
7
8
 
8
9
  Dry::Schema.load_extensions(:json_schema)
@@ -15,7 +16,7 @@ module Rager
15
16
  sig { params(schema: Dry::Schema::JSON).returns(T::Hash[Symbol, T.untyped]) }
16
17
  def self.dry_schema_to_json_schema(schema)
17
18
  json_schema_original = schema.json_schema
18
- json_schema = JSON.parse(JSON.generate(json_schema_original).force_encoding("UTF-8"))
19
+ json_schema = JSON.parse(JSON.generate(json_schema_original))
19
20
 
20
21
  make_strict_recursive!(json_schema)
21
22
 
@@ -41,8 +42,6 @@ module Rager
41
42
  node.each { |item| make_strict_recursive!(item) }
42
43
  end
43
44
  end
44
-
45
- private_class_method :make_strict_recursive!
46
45
  end
47
46
  end
48
47
  end
data/lib/rager/config.rb CHANGED
@@ -7,40 +7,36 @@ module Rager
7
7
  class Config
8
8
  extend T::Sig
9
9
 
10
- sig { returns(T.nilable(Rager::Logger)) }
11
- attr_accessor :logger_type
12
-
13
10
  sig { returns(::Logger) }
14
11
  attr_accessor :logger
15
12
 
13
+ sig { returns(T::Boolean) }
14
+ attr_accessor :log_raise
15
+
16
+ sig { returns(Rager::LogStrategy) }
17
+ attr_accessor :log_strategy
18
+
19
+ sig { returns(Rager::Http::Adapters::Abstract) }
20
+ attr_accessor :http_adapter
21
+
16
22
  sig { returns(T.nilable(String)) }
17
23
  attr_accessor :url
18
24
 
19
25
  sig { returns(T.nilable(String)) }
20
26
  attr_accessor :api_key
21
27
 
28
+ sig { returns(T.nilable(Numeric)) }
29
+ attr_accessor :timeout
30
+
22
31
  sig { void }
23
32
  def initialize
24
- @http_adapter = T.let(nil, T.nilable(Rager::Http::Adapters::Abstract))
25
- @logger_type = T.let(nil, T.nilable(Rager::Logger))
26
33
  @logger = T.let(::Logger.new($stdout), ::Logger)
34
+ @log_raise = T.let(false, T::Boolean)
35
+ @log_strategy = T.let(Rager::LogStrategy::None, Rager::LogStrategy)
36
+ @http_adapter = T.let(Rager::Http::Adapters::NetHttp.new, Rager::Http::Adapters::Abstract)
27
37
  @url = T.let(nil, T.nilable(String))
28
38
  @api_key = T.let(nil, T.nilable(String))
29
- end
30
-
31
- sig { returns(Rager::Http::Adapters::Abstract) }
32
- def http_adapter
33
- @http_adapter ||= default_http_adapter
34
- end
35
-
36
- sig { params(http_adapter: Rager::Http::Adapters::Abstract).void }
37
- attr_writer :http_adapter
38
-
39
- private
40
-
41
- sig { returns(Rager::Http::Adapters::Abstract) }
42
- def default_http_adapter
43
- Rager::Http::Adapters::AsyncHttp.new
39
+ @timeout = T.let(nil, T.nilable(Numeric))
44
40
  end
45
41
  end
46
42
  end