rager 0.6.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +26 -27
  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 +6 -7
  7. data/lib/rager/chat/providers/openai.rb +49 -34
  8. data/lib/rager/chat/schema.rb +0 -2
  9. data/lib/rager/config.rb +12 -20
  10. data/lib/rager/context.rb +302 -186
  11. data/lib/rager/context_options.rb +23 -0
  12. data/lib/rager/embed/options.rb +3 -3
  13. data/lib/rager/embed/providers/openai.rb +7 -3
  14. data/lib/rager/errors/options_error.rb +1 -1
  15. data/lib/rager/http/adapters/async_http.rb +0 -2
  16. data/lib/rager/http/adapters/mock.rb +0 -2
  17. data/lib/rager/http/adapters/net_http.rb +18 -19
  18. data/lib/rager/{image_gen → image}/options.rb +4 -4
  19. data/lib/rager/{image_gen → image}/output_format.rb +1 -1
  20. data/lib/rager/{image_gen → image}/providers/abstract.rb +4 -4
  21. data/lib/rager/{image_gen → image}/providers/replicate.rb +14 -10
  22. data/lib/rager/{logger.rb → log_strategy.rb} +2 -1
  23. data/lib/rager/{mesh_gen → mesh}/options.rb +3 -3
  24. data/lib/rager/{mesh_gen → mesh}/providers/abstract.rb +4 -4
  25. data/lib/rager/{mesh_gen → mesh}/providers/replicate.rb +13 -9
  26. data/lib/rager/operation.rb +2 -2
  27. data/lib/rager/options.rb +1 -1
  28. data/lib/rager/outcome.rb +25 -0
  29. data/lib/rager/providers.rb +37 -25
  30. data/lib/rager/rerank/input.rb +20 -0
  31. data/lib/rager/rerank/options.rb +2 -2
  32. data/lib/rager/rerank/providers/abstract.rb +2 -3
  33. data/lib/rager/rerank/providers/cohere.rb +19 -13
  34. data/lib/rager/rerank/result.rb +20 -0
  35. data/lib/rager/result.rb +92 -122
  36. data/lib/rager/search/options.rb +3 -2
  37. data/lib/rager/search/providers/jina.rb +16 -15
  38. data/lib/rager/search/result.rb +21 -0
  39. data/lib/rager/template/input.rb +20 -0
  40. data/lib/rager/template/options.rb +1 -1
  41. data/lib/rager/template/providers/abstract.rb +2 -3
  42. data/lib/rager/template/providers/erb.rb +4 -5
  43. data/lib/rager/template/providers/mustache.rb +30 -0
  44. data/lib/rager/types.rb +31 -24
  45. data/lib/rager/utils/http.rb +73 -15
  46. data/lib/rager/utils/replicate.rb +28 -6
  47. data/lib/rager/utils/runtime.rb +21 -0
  48. data/lib/rager/version.rb +1 -1
  49. metadata +18 -12
  50. data/lib/rager/rerank/output.rb +0 -13
  51. data/lib/rager/search/output.rb +0 -14
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2036814b6f6bbe713486b60918b0d75222416ecf90eeb2e0fbf139c44d2d8c1f
4
- data.tar.gz: d6e112b1c3d1189df8957aa219a0cee7d86735b4198ec355680fab25215e33aa
3
+ metadata.gz: a240f204b0677aee45009f9357cea5e13b85eaa77dd9696db6e12e065f0144b4
4
+ data.tar.gz: 8d31692e3d8fe5d962ff4f2a9a94817a8886dd20f01eb72cbb24189deb490365
5
5
  SHA512:
6
- metadata.gz: f3c65b99f301a2e617a86c1bbd2ae3149cd1a7a473d688936f66e4321eaf1ead643f58e9d387dd7d9cb314e15188a4577f710188da3082c5753229a1ae8998bf
7
- data.tar.gz: 80e6f853ccdee991e19ba3f921fc6b10023cc2c2620d92ccc40f9b796cd5037d2ddad694834d0df4e9ed5aa9fdd84554c97a8afc2b311c95f7cb94c565a56f95
6
+ metadata.gz: 381d1ee7c8202be21536d209d3845039223709e7dd4e93c974f1970ce4b019f8538cac0bcb6cae7c8c15209e86bde9d6adb0e10d9e9b4546734744d4be7133dd
7
+ data.tar.gz: 34341e107e1018810723d9bdbd5f6e0b6fc69f66daf166860aec9f4513b29827479754ba84afe07cf5e9ece45cacf23c0e34f70f8fe896fd14c2b99d86a9f878
data/README.md CHANGED
@@ -8,41 +8,25 @@
8
8
 
9
9
  Build continuously improving generative workflows.
10
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.
11
+ Examples are available in the [`examples/`](./examples/) folder.
26
12
 
27
13
  ## Installation
28
14
 
29
- Add this line to your applications Gemfile:
15
+ Add this line to your application's Gemfile:
30
16
 
31
17
  ```ruby
32
- gem "rager", "~> 0.6.0"
18
+ gem "rager", "~> 0.7.0"
33
19
  ```
34
20
 
35
- Or use it in a standalone script (example uses OPENAI_API_KEY env var):
21
+ Or use it in a standalone script (requires OPENAI_API_KEY set):
36
22
 
37
23
  ```ruby
38
- #!/usr/bin/env ruby
39
-
40
24
  require "bundler/inline"
41
25
 
42
26
  gemfile do
43
27
  source "https://rubygems.org"
44
28
  gem "async-http", "~> 0.88.0"
45
- gem "rager", "~> 0.6.0"
29
+ gem "rager", "~> 0.7.0"
46
30
  end
47
31
 
48
32
  require "rager"
@@ -51,18 +35,33 @@ Rager.configure do |config|
51
35
  config.http_adapter = Rager::Http::Adapters::AsyncHttp.new
52
36
  end
53
37
 
38
+ PROMPT = ARGV[0] || "Ruby programming"
39
+
54
40
  Async do
55
41
  ctx = Rager::Context.new
56
- prompt = ctx.template(
57
- "Tell me about the history of <%= topic %>:\n",
58
- {topic: "Ruby programming"}
59
- )
42
+ prompt = ctx.template("Tell me about the history of <%= topic %>",{topic: PROMPT})
60
43
  chat = ctx.chat(prompt, stream: true)
61
- chat.out.each { |d| print d.content }
44
+ chat.stream.each { |d| print d.content }
62
45
  end
63
46
  ```
64
47
 
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).
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.
66
65
 
67
66
  ## License
68
67
 
@@ -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,22 +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)
15
+ const :stream, T.nilable(T::Boolean)
18
16
  const :n, T.nilable(Integer)
19
- const :max_tokens, 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
- const :stream, T.nilable(T::Boolean)
25
22
  const :seed, T.nilable(Integer)
23
+ const :url, T.nilable(String)
24
+ const :api_key, T.nilable(String)
26
25
  const :timeout, T.nilable(Numeric)
27
26
 
28
- sig { override.returns(T::Hash[String, T.untyped]) }
27
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
29
28
  def serialize_safe
30
29
  result = serialize
31
30
  result["api_key"] = "[REDACTED]" if result.key?("api_key")
@@ -11,7 +11,8 @@ module Rager
11
11
  class Openai < Rager::Chat::Providers::Abstract
12
12
  extend T::Sig
13
13
 
14
- 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])]] }
15
16
 
16
17
  sig do
17
18
  override.params(
@@ -24,10 +25,17 @@ module Rager
24
25
  raise Rager::Errors::CredentialsError.new("OpenAI", env_var: ["OPENAI_API_KEY"]) if api_key.nil?
25
26
 
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
27
35
 
28
36
  body = {
29
37
  model: options.model || "gpt-4.1",
30
- messages: build_openai_messages(messages, options.history, options.system_prompt)
38
+ messages: build_openai_messages(messages, options.system_prompt)
31
39
  }.tap do |b|
32
40
  b[:n] = options.n if options.n
33
41
  b[:max_tokens] = options.max_tokens if options.max_tokens
@@ -47,12 +55,9 @@ module Rager
47
55
  end
48
56
  end
49
57
 
50
- headers = {"Content-Type" => "application/json"}
51
- headers["Authorization"] = "Bearer #{api_key}" if api_key
52
-
53
58
  request = Rager::Http::Request.new(
54
59
  verb: Rager::Http::Verb::Post,
55
- url: "#{base_url}/chat/completions",
60
+ url: url,
56
61
  headers: headers,
57
62
  body: body.to_json,
58
63
  streaming: options.stream || false,
@@ -70,39 +75,36 @@ module Rager
70
75
  end
71
76
  end
72
77
 
73
- private
74
-
75
78
  sig do
76
79
  params(
77
80
  messages: T::Array[Rager::Chat::Message],
78
- history: T::Array[Rager::Chat::Message],
79
81
  system_prompt: T.nilable(String)
80
82
  ).returns(OpenaiMessages)
81
83
  end
82
- def build_openai_messages(messages, history, system_prompt)
83
- result = T.let([], OpenaiMessages)
84
+ def build_openai_messages(messages, system_prompt)
85
+ output = T.let([], OpenaiMessages)
84
86
 
85
- if history.empty? && system_prompt && !system_prompt.empty?
86
- result << {"role" => "system", "content" => system_prompt}
87
+ if system_prompt
88
+ output << {"role" => "system", "content" => system_prompt}
87
89
  end
88
90
 
89
- (history + messages).each do |message|
91
+ messages.each do |message|
90
92
  role = message.role.is_a?(String) ? message.role : message.role.serialize
91
93
  content = message.content
92
94
 
93
95
  case content
94
96
  when String
95
- result << {"role" => role, "content" => content}
97
+ output << {"role" => role, "content" => content}
96
98
  when Array
97
99
  formatted_content = content.map { |item| format_content_item(item) }
98
- result << {"role" => role, "content" => formatted_content}
100
+ output << {"role" => role, "content" => formatted_content}
99
101
  end
100
102
  end
101
103
 
102
- result
104
+ output
103
105
  end
104
106
 
105
- sig { params(item: Rager::Chat::MessageContent).returns(T::Hash[String, T.untyped]) }
107
+ sig { params(item: Rager::Chat::MessageContent).returns(OpenaiContentItem) }
106
108
  def format_content_item(item)
107
109
  case item.type
108
110
  when Rager::Chat::MessageContentType::Text
@@ -119,7 +121,7 @@ module Rager
119
121
  end
120
122
  end
121
123
 
122
- sig { params(body: String).returns(Rager::Types::ChatNonStream) }
124
+ sig { params(body: String).returns(Rager::Types::ChatNonStreamOutput) }
123
125
  def handle_non_stream_body(body)
124
126
  response_data = JSON.parse(body)
125
127
  return [] unless response_data.key?("choices") && response_data["choices"].is_a?(Array)
@@ -137,25 +139,38 @@ module Rager
137
139
  sig { params(body: T::Enumerator[String]).returns(T::Enumerator[Rager::Chat::MessageDelta]) }
138
140
  def handle_stream_body(body)
139
141
  Enumerator.new do |yielder|
140
- buffer = +""
142
+ buffer_parts = []
141
143
  body.each do |chunk|
142
- buffer << chunk
144
+ buffer_parts << chunk.force_encoding(Encoding::UTF_8)
145
+ buffer = buffer_parts.join
143
146
  while (line = buffer.slice!(/.*\n/))
144
- line.delete_prefix!("data: ") or next
145
- line.strip!
146
- next if line.empty? || line == "[DONE]"
147
-
148
- begin
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
153
- end
154
- rescue
155
- nil
156
- end
147
+ process_stream_line(line, yielder)
148
+ end
149
+ buffer_parts = buffer.empty? ? [] : [buffer]
150
+ end
151
+
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)
157
170
  end
158
171
  end
172
+ rescue JSON::ParserError
173
+ nil
159
174
  end
160
175
  end
161
176
  end
@@ -42,8 +42,6 @@ module Rager
42
42
  node.each { |item| make_strict_recursive!(item) }
43
43
  end
44
44
  end
45
-
46
- private_class_method :make_strict_recursive!
47
45
  end
48
46
  end
49
47
  end
data/lib/rager/config.rb CHANGED
@@ -7,12 +7,18 @@ 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
 
@@ -24,27 +30,13 @@ module Rager
24
30
 
25
31
  sig { void }
26
32
  def initialize
27
- @http_adapter = T.let(nil, T.nilable(Rager::Http::Adapters::Abstract))
28
- @logger_type = T.let(nil, T.nilable(Rager::Logger))
29
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)
30
37
  @url = T.let(nil, T.nilable(String))
31
38
  @api_key = T.let(nil, T.nilable(String))
32
39
  @timeout = T.let(nil, T.nilable(Numeric))
33
40
  end
34
-
35
- sig { returns(Rager::Http::Adapters::Abstract) }
36
- def http_adapter
37
- @http_adapter ||= default_http_adapter
38
- end
39
-
40
- sig { params(http_adapter: Rager::Http::Adapters::Abstract).void }
41
- attr_writer :http_adapter
42
-
43
- private
44
-
45
- sig { returns(Rager::Http::Adapters::Abstract) }
46
- def default_http_adapter
47
- Rager::Http::Adapters::NetHttp.new
48
- end
49
41
  end
50
42
  end