rager 0.5.0 → 0.6.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +50 -7
  3. data/lib/rager/chat/options.rb +9 -5
  4. data/lib/rager/chat/providers/openai.rb +27 -26
  5. data/lib/rager/chat/schema.rb +3 -2
  6. data/lib/rager/config.rb +5 -1
  7. data/lib/rager/context.rb +168 -55
  8. data/lib/rager/embed/options.rb +1 -0
  9. data/lib/rager/embed/providers/openai.rb +8 -4
  10. data/lib/rager/errors/credentials_error.rb +24 -0
  11. data/lib/rager/errors/dependency_error.rb +23 -0
  12. data/lib/rager/errors/http_error.rb +12 -5
  13. data/lib/rager/errors/options_error.rb +10 -5
  14. data/lib/rager/errors/parse_error.rb +9 -5
  15. data/lib/rager/errors/template_error.rb +10 -4
  16. data/lib/rager/errors/timeout_error.rb +25 -0
  17. data/lib/rager/http/adapters/async_http.rb +67 -13
  18. data/lib/rager/http/adapters/mock.rb +43 -45
  19. data/lib/rager/http/adapters/net_http.rb +145 -0
  20. data/lib/rager/http/request.rb +2 -0
  21. data/lib/rager/image_gen/options.rb +1 -0
  22. data/lib/rager/image_gen/providers/replicate.rb +5 -4
  23. data/lib/rager/mesh_gen/options.rb +1 -0
  24. data/lib/rager/mesh_gen/providers/replicate.rb +7 -5
  25. data/lib/rager/providers.rb +49 -0
  26. data/lib/rager/rerank/options.rb +1 -0
  27. data/lib/rager/rerank/{query.rb → output.rb} +2 -2
  28. data/lib/rager/rerank/providers/abstract.rb +3 -2
  29. data/lib/rager/rerank/providers/cohere.rb +17 -14
  30. data/lib/rager/result.rb +49 -29
  31. data/lib/rager/search/options.rb +3 -1
  32. data/lib/rager/search/output.rb +14 -0
  33. data/lib/rager/search/providers/jina.rb +67 -0
  34. data/lib/rager/template/providers/abstract.rb +3 -2
  35. data/lib/rager/template/providers/erb.rb +6 -5
  36. data/lib/rager/types.rb +11 -7
  37. data/lib/rager/utils/http.rb +35 -28
  38. data/lib/rager/utils/replicate.rb +13 -16
  39. data/lib/rager/version.rb +1 -1
  40. metadata +10 -30
  41. data/lib/rager/chat.rb +0 -35
  42. data/lib/rager/embed.rb +0 -35
  43. data/lib/rager/errors/missing_credentials_error.rb +0 -19
  44. data/lib/rager/errors/unknown_provider_error.rb +0 -17
  45. data/lib/rager/image_gen.rb +0 -31
  46. data/lib/rager/mesh_gen.rb +0 -31
  47. data/lib/rager/rerank/result.rb +0 -13
  48. data/lib/rager/rerank.rb +0 -35
  49. data/lib/rager/search/providers/brave.rb +0 -59
  50. data/lib/rager/search/result.rb +0 -14
  51. data/lib/rager/search.rb +0 -35
  52. data/lib/rager/template/input.rb +0 -11
  53. 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: 2036814b6f6bbe713486b60918b0d75222416ecf90eeb2e0fbf139c44d2d8c1f
4
+ data.tar.gz: d6e112b1c3d1189df8957aa219a0cee7d86735b4198ec355680fab25215e33aa
5
5
  SHA512:
6
- metadata.gz: 6dfa77b1500bc0a2693141bff0c6551b72448d100f53f0e612b572d3b894ad5acc09f2cf3bb727d12141511bf902d28b3d43b2e359afc5d3fc9f172aae7caa06
7
- data.tar.gz: d9522f23cd419a3b747f5e97e6097fc751008aedc3111954194de0253a24813a1919e0c5720cb653e51407e41173d80080461dadadb4590b716f638ea755444f
6
+ metadata.gz: f3c65b99f301a2e617a86c1bbd2ae3149cd1a7a473d688936f66e4321eaf1ead643f58e9d387dd7d9cb314e15188a4577f710188da3082c5753229a1ae8998bf
7
+ data.tar.gz: 80e6f853ccdee991e19ba3f921fc6b10023cc2c2620d92ccc40f9b796cd5037d2ddad694834d0df4e9ed5aa9fdd84554c97a8afc2b311c95f7cb94c565a56f95
data/README.md CHANGED
@@ -4,23 +4,66 @@
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
+ ## Providers
12
+
13
+ | Feature | Providers |
14
+ | ---------------------- | ------------------------------ |
15
+ | **Chat** | `openai` (and compatible APIs) |
16
+ | **Embedding** | `openai` (and compatible APIs) |
17
+ | **Image generation** | `replicate` |
18
+ | **3D mesh generation** | `replicate` |
19
+ | **Rerank** | `cohere` (and compatible APIs) |
20
+ | **Search** | `jina` |
21
+ | **Templating** | `erb` (built-in) |
22
+
23
+ ## Logging
24
+
25
+ The main reason for developing yet another library is to have an out-of-the-box compatible logging server that tracks AI workflows and outcomes. This data can be used to generate high-quality examples for few-shot prompting and fine-tuning. That server will be released soon, check back here for updates.
9
26
 
10
27
  ## Installation
11
28
 
12
- If you are using Bundler:
29
+ Add this line to your application’s Gemfile:
13
30
 
14
- ```bash
15
- bundle add rager
31
+ ```ruby
32
+ gem "rager", "~> 0.6.0"
16
33
  ```
17
34
 
18
- Otherwise you can add it to your Gemfile directly:
35
+ Or use it in a standalone script (example uses OPENAI_API_KEY env var):
36
+
37
+ ```ruby
38
+ #!/usr/bin/env ruby
19
39
 
20
- ```Ruby
21
- gem "rager", "~> 0.4.0"
40
+ require "bundler/inline"
41
+
42
+ gemfile do
43
+ source "https://rubygems.org"
44
+ gem "async-http", "~> 0.88.0"
45
+ gem "rager", "~> 0.6.0"
46
+ end
47
+
48
+ require "rager"
49
+
50
+ Rager.configure do |config|
51
+ config.http_adapter = Rager::Http::Adapters::AsyncHttp.new
52
+ end
53
+
54
+ Async do
55
+ ctx = Rager::Context.new
56
+ prompt = ctx.template(
57
+ "Tell me about the history of <%= topic %>:\n",
58
+ {topic: "Ruby programming"}
59
+ )
60
+ chat = ctx.chat(prompt, stream: true)
61
+ chat.out.each { |d| print d.content }
62
+ end
22
63
  ```
23
64
 
65
+ This library makes extensive use of `sorbet-runtime` for runtime type checking. To learn more, including how to disable it, please see the official Sorbet [documentation](https://sorbet.org/docs/overview).
66
+
24
67
  ## License
25
68
 
26
69
  [MIT](./LICENSE.md)
@@ -15,13 +15,15 @@ module Rager
15
15
  const :url, T.nilable(String)
16
16
  const :api_key, T.nilable(String)
17
17
  const :model, T.nilable(String)
18
- const :stream, T.nilable(T::Boolean)
19
18
  const :n, T.nilable(Integer)
19
+ const :max_tokens, T.nilable(Integer)
20
20
  const :temperature, T.nilable(Float)
21
21
  const :system_prompt, T.nilable(String)
22
22
  const :schema, T.nilable(Dry::Schema::JSON)
23
23
  const :schema_name, T.nilable(String)
24
+ const :stream, T.nilable(T::Boolean)
24
25
  const :seed, T.nilable(Integer)
26
+ const :timeout, T.nilable(Numeric)
25
27
 
26
28
  sig { override.returns(T::Hash[String, T.untyped]) }
27
29
  def serialize_safe
@@ -35,15 +37,17 @@ module Rager
35
37
  def validate
36
38
  if stream && schema
37
39
  raise Rager::Errors::OptionsError.new(
38
- invalid_keys: %w[stream schema],
39
- description: "You cannot use streaming with structured outputs"
40
+ self,
41
+ ["stream", "schema"],
42
+ details: "You cannot use streaming with structured outputs"
40
43
  )
41
44
  end
42
45
 
43
46
  if schema && schema_name.nil?
44
47
  raise Rager::Errors::OptionsError.new(
45
- invalid_keys: %w[schema schema_name],
46
- description: "You must provide a schema name when using structured outputs"
48
+ self,
49
+ ["schema", "schema_name"],
50
+ details: "You must provide a schema name when using structured outputs"
47
51
  )
48
52
  end
49
53
  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
@@ -20,14 +21,17 @@ module Rager
20
21
  end
21
22
  def chat(messages, options)
22
23
  api_key = options.api_key || ENV["OPENAI_API_KEY"]
23
- raise Rager::Errors::MissingCredentialsError.new("OpenAI", "OPENAI_API_KEY") if api_key.nil?
24
+ raise Rager::Errors::CredentialsError.new("OpenAI", env_var: ["OPENAI_API_KEY"]) if api_key.nil?
25
+
26
+ base_url = options.url || ENV["OPENAI_URL"] || "https://api.openai.com/v1"
24
27
 
25
28
  body = {
26
29
  model: options.model || "gpt-4.1",
27
30
  messages: build_openai_messages(messages, options.history, options.system_prompt)
28
31
  }.tap do |b|
29
- b[:temperature] = options.temperature if options.temperature
30
32
  b[:n] = options.n if options.n
33
+ b[:max_tokens] = options.max_tokens if options.max_tokens
34
+ b[:temperature] = options.temperature if options.temperature
31
35
  b[:stream] = options.stream if options.stream
32
36
  b[:seed] = options.seed if options.seed
33
37
 
@@ -48,15 +52,17 @@ module Rager
48
52
 
49
53
  request = Rager::Http::Request.new(
50
54
  verb: Rager::Http::Verb::Post,
51
- url: options.url || ENV["OPENAI_URL"] || "https://api.openai.com/v1/chat/completions",
55
+ url: "#{base_url}/chat/completions",
52
56
  headers: headers,
53
- body: body.to_json
57
+ body: body.to_json,
58
+ streaming: options.stream || false,
59
+ timeout: options.timeout || Rager.config.timeout
54
60
  )
55
61
 
56
62
  response = Rager.config.http_adapter.make_request(request)
57
63
  response_body = T.must(response.body)
58
64
 
59
- raise Rager::Errors::HttpError.new(Rager.config.http_adapter, response.status, T.cast(response_body, String)) if response.status != 200
65
+ raise Rager::Errors::HttpError.new(Rager.config.http_adapter, request.url, response.status, body: T.cast(response_body, String)) if response.status != 200
60
66
 
61
67
  case response_body
62
68
  when String then handle_non_stream_body(response_body)
@@ -113,48 +119,43 @@ module Rager
113
119
  end
114
120
  end
115
121
 
116
- sig { params(body: String).returns(T::Array[String]) }
122
+ sig { params(body: String).returns(Rager::Types::ChatNonStream) }
117
123
  def handle_non_stream_body(body)
118
124
  response_data = JSON.parse(body)
119
125
  return [] unless response_data.key?("choices") && response_data["choices"].is_a?(Array)
120
126
 
121
- response_data["choices"].filter_map do |choice|
127
+ result = response_data["choices"].filter_map do |choice|
122
128
  text = choice.dig("message", "content").to_s
123
129
  text unless text.empty?
124
130
  end
131
+
132
+ result.one? ? result.first : result
125
133
  rescue JSON::ParserError
126
- raise Rager::Errors::ParseError.new("OpenAI response body is not valid JSON", body)
134
+ raise Rager::Errors::ParseError.new(body, details: "OpenAI response body is not valid JSON")
127
135
  end
128
136
 
129
137
  sig { params(body: T::Enumerator[String]).returns(T::Enumerator[Rager::Chat::MessageDelta]) }
130
138
  def handle_stream_body(body)
131
139
  Enumerator.new do |yielder|
132
140
  buffer = +""
133
-
134
- process_chunk = ->(chunk) do
141
+ body.each do |chunk|
135
142
  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]"
143
+ while (line = buffer.slice!(/.*\n/))
144
+ line.delete_prefix!("data: ") or next
145
+ line.strip!
146
+ next if line.empty? || line == "[DONE]"
141
147
 
142
148
  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
+ JSON.parse(line).dig("choices")&.each do |choice|
150
+ if (content = choice.dig("delta", "content"))
151
+ yielder << Rager::Chat::MessageDelta.new(index: choice["index"] || 0, content: content)
152
+ end
149
153
  end
150
- rescue JSON::ParserError
151
- next
154
+ rescue
155
+ nil
152
156
  end
153
157
  end
154
158
  end
155
-
156
- body.each(&process_chunk)
157
- process_chunk.call("\n") unless buffer.empty?
158
159
  end
159
160
  end
160
161
  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
 
data/lib/rager/config.rb CHANGED
@@ -19,6 +19,9 @@ module Rager
19
19
  sig { returns(T.nilable(String)) }
20
20
  attr_accessor :api_key
21
21
 
22
+ sig { returns(T.nilable(Numeric)) }
23
+ attr_accessor :timeout
24
+
22
25
  sig { void }
23
26
  def initialize
24
27
  @http_adapter = T.let(nil, T.nilable(Rager::Http::Adapters::Abstract))
@@ -26,6 +29,7 @@ module Rager
26
29
  @logger = T.let(::Logger.new($stdout), ::Logger)
27
30
  @url = T.let(nil, T.nilable(String))
28
31
  @api_key = T.let(nil, T.nilable(String))
32
+ @timeout = T.let(nil, T.nilable(Numeric))
29
33
  end
30
34
 
31
35
  sig { returns(Rager::Http::Adapters::Abstract) }
@@ -40,7 +44,7 @@ module Rager
40
44
 
41
45
  sig { returns(Rager::Http::Adapters::Abstract) }
42
46
  def default_http_adapter
43
- Rager::Http::Adapters::AsyncHttp.new
47
+ Rager::Http::Adapters::NetHttp.new
44
48
  end
45
49
  end
46
50
  end
data/lib/rager/context.rb CHANGED
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "securerandom"
5
+
5
6
  require "sorbet-runtime"
6
7
 
7
8
  module Rager
@@ -14,58 +15,76 @@ module Rager
14
15
  sig { returns(T.nilable(String)) }
15
16
  attr_reader :name
16
17
 
17
- sig { params(id: T.nilable(String), name: T.nilable(String)).void }
18
- def initialize(id: nil, name: nil)
18
+ sig { returns(Integer) }
19
+ attr_reader :max_retries
20
+
21
+ sig { returns(Float) }
22
+ attr_reader :backoff
23
+
24
+ sig { params(id: T.nilable(String), name: T.nilable(String), max_retries: T.nilable(Integer), backoff: T.nilable(Float)).void }
25
+ def initialize(id: nil, name: nil, max_retries: nil, backoff: nil)
19
26
  @id = T.let(id || SecureRandom.uuid, String)
20
27
  @name = T.let(name, T.nilable(String))
28
+ @max_retries = T.let(max_retries || 0, Integer)
29
+ @backoff = T.let(backoff || 1.0, Float)
21
30
  end
22
31
 
23
32
  sig do
24
33
  params(
25
- messages: T.any(String, Rager::Types::ChatInput),
34
+ messages: T.any(String, Rager::Types::ChatInput, Rager::Result),
26
35
  kwargs: T.untyped
27
36
  ).returns(Rager::Result)
28
37
  end
29
38
  def chat(messages, **kwargs)
30
- if messages.is_a?(String)
31
- messages = [
32
- Rager::Chat::Message.new(
33
- role: Rager::Chat::MessageRole::User,
34
- content: messages
35
- )
36
- ]
37
- end
38
-
39
39
  execute(
40
40
  Rager::Operation::Chat,
41
41
  Rager::Chat::Options,
42
42
  kwargs,
43
43
  messages
44
- ) { |options| Chat.chat(messages, options) }
44
+ ) { |options, normalized_input|
45
+ final_input = if normalized_input.is_a?(String)
46
+ [
47
+ Rager::Chat::Message.new(
48
+ role: Rager::Chat::MessageRole::User,
49
+ content: normalized_input
50
+ )
51
+ ]
52
+ else
53
+ normalized_input
54
+ end
55
+
56
+ provider = Rager::Providers.get_provider(:chat, options.provider, options)
57
+ provider.chat(final_input, options)
58
+ }
45
59
  end
46
60
 
47
61
  sig do
48
62
  params(
49
- text: T.any(String, Rager::Types::EmbedInput),
63
+ text: T.any(String, Rager::Types::EmbedInput, Rager::Result),
50
64
  kwargs: T.untyped
51
65
  ).returns(Rager::Result)
52
66
  end
53
67
  def embed(text, **kwargs)
54
- if text.is_a?(String)
55
- text = [text]
56
- end
57
-
58
68
  execute(
59
69
  Rager::Operation::Embed,
60
70
  Rager::Embed::Options,
61
71
  kwargs,
62
72
  text
63
- ) { |options| Embed.embed(text, options) }
73
+ ) { |options, normalized_input|
74
+ final_input = if normalized_input.is_a?(String)
75
+ [normalized_input]
76
+ else
77
+ normalized_input
78
+ end
79
+
80
+ provider = Rager::Providers.get_provider(:embed, options.provider, options)
81
+ provider.embed(final_input, options)
82
+ }
64
83
  end
65
84
 
66
85
  sig do
67
86
  params(
68
- prompt: Rager::Types::ImageGenInput,
87
+ prompt: T.any(Rager::Types::ImageGenInput, Rager::Result),
69
88
  kwargs: T.untyped
70
89
  ).returns(Rager::Result)
71
90
  end
@@ -75,12 +94,15 @@ module Rager
75
94
  Rager::ImageGen::Options,
76
95
  kwargs,
77
96
  prompt
78
- ) { |options| ImageGen.image_gen(prompt, options) }
97
+ ) { |options, normalized_input|
98
+ provider = Rager::Providers.get_provider(:image_gen, options.provider, options)
99
+ provider.image_gen(T.cast(normalized_input, Rager::Types::ImageGenInput), options)
100
+ }
79
101
  end
80
102
 
81
103
  sig do
82
104
  params(
83
- prompt: Rager::Types::MeshGenInput,
105
+ prompt: T.any(Rager::Types::MeshGenInput, Rager::Result),
84
106
  kwargs: T.untyped
85
107
  ).returns(Rager::Result)
86
108
  end
@@ -90,27 +112,35 @@ module Rager
90
112
  Rager::MeshGen::Options,
91
113
  kwargs,
92
114
  prompt
93
- ) { |options| MeshGen.mesh_gen(prompt, options) }
115
+ ) { |options, normalized_input|
116
+ provider = Rager::Providers.get_provider(:mesh_gen, options.provider, options)
117
+ provider.mesh_gen(T.cast(normalized_input, Rager::Types::MeshGenInput), options)
118
+ }
94
119
  end
95
120
 
96
121
  sig do
97
122
  params(
98
- query: Rager::Types::RerankInput,
123
+ query: T.any(String, Rager::Result),
124
+ documents: T.any(T::Array[String], Rager::Result),
99
125
  kwargs: T.untyped
100
126
  ).returns(Rager::Result)
101
127
  end
102
- def rerank(query, **kwargs)
128
+ def rerank(query, documents, **kwargs)
103
129
  execute(
104
130
  Rager::Operation::Rerank,
105
131
  Rager::Rerank::Options,
106
132
  kwargs,
107
- query
108
- ) { |options| Rager::Rerank.rerank(query, options) }
133
+ {query: query, documents: documents}
134
+ ) { |options, normalized_input|
135
+ provider = Rager::Providers.get_provider(:rerank, options.provider, options)
136
+ input_hash = T.cast(normalized_input, T::Hash[Symbol, T.untyped])
137
+ provider.rerank(input_hash[:query], input_hash[:documents], options)
138
+ }
109
139
  end
110
140
 
111
141
  sig do
112
142
  params(
113
- query: Rager::Types::SearchInput,
143
+ query: T.any(Rager::Types::SearchInput, Rager::Result),
114
144
  kwargs: T.untyped
115
145
  ).returns(Rager::Result)
116
146
  end
@@ -120,22 +150,30 @@ module Rager
120
150
  Rager::Search::Options,
121
151
  kwargs,
122
152
  query
123
- ) { |options| Search.search(query, options) }
153
+ ) { |options, normalized_input|
154
+ provider = Rager::Providers.get_provider(:search, options.provider, options)
155
+ provider.search(T.cast(normalized_input, Rager::Types::SearchInput), options)
156
+ }
124
157
  end
125
158
 
126
159
  sig do
127
160
  params(
128
- input: Rager::Types::TemplateInput,
161
+ template: T.any(String, Rager::Result),
162
+ variables: T.any(T::Hash[Symbol, T.untyped], Rager::Result),
129
163
  kwargs: T.untyped
130
164
  ).returns(Rager::Result)
131
165
  end
132
- def template(input, **kwargs)
166
+ def template(template, variables, **kwargs)
133
167
  execute(
134
168
  Rager::Operation::Template,
135
169
  Rager::Template::Options,
136
170
  kwargs,
137
- input
138
- ) { |options| Template.template(input, options) }
171
+ {template: template, variables: variables}
172
+ ) { |options, normalized_input|
173
+ provider = Rager::Providers.get_provider(:template, options.provider, options)
174
+ input_hash = T.cast(normalized_input, T::Hash[Symbol, T.untyped])
175
+ provider.template(input_hash[:template], input_hash[:variables], options)
176
+ }
139
177
  end
140
178
 
141
179
  private
@@ -145,53 +183,128 @@ module Rager
145
183
  operation: Rager::Operation,
146
184
  options_struct: T::Class[Rager::Options],
147
185
  kwargs: T.untyped,
148
- input: Rager::Types::Input,
149
- block: T.proc.params(options: T.untyped).returns(T.untyped)
186
+ input: T.any(Rager::Types::Input, Rager::Result, T::Hash[Symbol, T.untyped]),
187
+ block: T.proc.params(options: T.untyped, normalized_input: Rager::Types::Input).returns(T.untyped)
150
188
  ).returns(Rager::Result)
151
189
  end
152
190
  def execute(operation, options_struct, kwargs, input, &block)
153
191
  name = kwargs.delete(:name)
154
- iids = kwargs.delete(:iids)
192
+ tags = kwargs.delete(:tags) || []
193
+
194
+ existing_input_ids = kwargs.delete(:input_ids) || []
195
+ normalized_input, new_input_ids = normalize_input(input)
196
+ input_ids = (existing_input_ids + new_input_ids).uniq
155
197
 
156
198
  options = options_struct.new(**kwargs)
157
199
  options.validate
158
200
 
159
201
  start_time = Time.now
202
+ errors = []
203
+ attempt = 0
160
204
 
161
205
  begin
162
- output = yield(options)
206
+ output = yield(options, normalized_input)
163
207
 
164
208
  Result.new(
165
209
  id: SecureRandom.uuid,
166
210
  context_id: @id,
167
211
  operation: operation,
168
- input: input,
212
+ input: normalized_input,
169
213
  output: output,
170
214
  options: options,
171
215
  start_time: start_time.to_i,
172
216
  end_time: Time.now.to_i,
173
217
  name: name,
174
218
  context_name: @name,
175
- iids: iids,
176
- error: nil
219
+ tags: tags,
220
+ input_ids: input_ids,
221
+ errors: errors,
222
+ attempt: attempt
177
223
  ).tap(&:log)
178
224
  rescue => e
179
- Result.new(
180
- id: SecureRandom.uuid,
181
- context_id: @id,
182
- operation: operation,
183
- input: input,
184
- output: nil,
185
- options: options,
186
- start_time: start_time.to_i,
187
- end_time: Time.now.to_i,
188
- name: name,
189
- context_name: @name,
190
- iids: iids,
191
- error: e.message
192
- ).tap(&:log)
225
+ errors << e.message
226
+ attempt += 1
227
+
228
+ if attempt < @max_retries
229
+ delay = @backoff * 2**attempt
230
+ Rager::Utils::Http.sleep(delay)
231
+ retry
232
+ else
233
+ Result.new(
234
+ id: SecureRandom.uuid,
235
+ context_id: @id,
236
+ operation: operation,
237
+ input: normalized_input,
238
+ output: nil,
239
+ options: options,
240
+ start_time: start_time.to_i,
241
+ end_time: Time.now.to_i,
242
+ name: name,
243
+ context_name: @name,
244
+ tags: tags,
245
+ input_ids: input_ids,
246
+ errors: errors,
247
+ attempt: attempt
248
+ ).tap(&:log)
249
+
250
+ raise e
251
+ end
252
+ end
253
+ end
254
+
255
+ sig { params(input: T.untyped, ids: T::Array[String]).returns([T.untyped, T::Array[String]]) }
256
+ def normalize_input(input, ids: [])
257
+ case input
258
+ when Rager::Result
259
+ ids << input.id unless ids.include?(input.id)
260
+
261
+ materialized_output = input.mat
262
+ normalized_output = case materialized_output
263
+ when Rager::Search::Output
264
+ materialized_output.contents
265
+ when Rager::Rerank::Output
266
+ materialized_output.documents
267
+ else
268
+ materialized_output
269
+ end
270
+
271
+ [normalized_output, ids]
272
+ when Hash
273
+ normalized_hash = {}
274
+ input.each do |key, value|
275
+ normalized_value, ids = normalize_input(value, ids: ids)
276
+ normalized_hash[key] = normalized_value
277
+ end
278
+ [normalized_hash, ids]
279
+ when Array
280
+ normalized_array = []
281
+ input.each do |item|
282
+ normalized_item, ids = normalize_input(item, ids: ids)
283
+ normalized_array << normalized_item
284
+ end
285
+ [normalized_array, ids]
286
+ else
287
+ [input, ids]
288
+ end
289
+ end
290
+
291
+ sig { returns(String) }
292
+ def to_json
293
+ {
294
+ id: @id,
295
+ name: @name,
296
+ max_retries: @max_retries,
297
+ backoff: @backoff
298
+ }.to_json
299
+ end
193
300
 
194
- raise e
301
+ sig { params(json: String, id: T.nilable(String), name: T.nilable(String), max_retries: T.nilable(Integer), backoff: T.nilable(Float)).returns(Rager::Context) }
302
+ def from_json(json, id: nil, name: nil, max_retries: nil, backoff: nil)
303
+ JSON.parse(json, symbolize_names: true).tap do |data|
304
+ @id = id || data[:id]
305
+ @name = name || data[:name]
306
+ @max_retries = max_retries || data[:max_retries]
307
+ @backoff = backoff || data[:backoff]
195
308
  end
196
309
  end
197
310
  end
@@ -14,6 +14,7 @@ module Rager
14
14
  const :model, T.nilable(String)
15
15
  const :api_key, T.nilable(String)
16
16
  const :seed, T.nilable(Integer)
17
+ const :timeout, T.nilable(Numeric)
17
18
  end
18
19
  end
19
20
  end