rager 0.6.0 → 0.8.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.
- checksums.yaml +4 -4
- data/README.md +22 -25
- data/lib/rager/chat/message.rb +10 -0
- data/lib/rager/chat/message_content.rb +7 -0
- data/lib/rager/chat/message_delta.rb +7 -0
- data/lib/rager/chat/options.rb +6 -7
- data/lib/rager/chat/providers/openai.rb +49 -34
- data/lib/rager/chat/schema.rb +0 -2
- data/lib/rager/config.rb +12 -20
- data/lib/rager/context.rb +302 -186
- data/lib/rager/context_options.rb +23 -0
- data/lib/rager/embed/options.rb +3 -3
- data/lib/rager/embed/providers/openai.rb +7 -3
- data/lib/rager/errors/options_error.rb +1 -1
- data/lib/rager/http/adapters/async_http.rb +0 -2
- data/lib/rager/http/adapters/mock.rb +0 -2
- data/lib/rager/http/adapters/net_http.rb +18 -19
- data/lib/rager/{image_gen → image}/options.rb +4 -4
- data/lib/rager/{image_gen → image}/output_format.rb +1 -1
- data/lib/rager/{image_gen → image}/providers/abstract.rb +4 -4
- data/lib/rager/{image_gen → image}/providers/replicate.rb +14 -10
- data/lib/rager/{logger.rb → log_strategy.rb} +2 -1
- data/lib/rager/{mesh_gen → mesh}/options.rb +3 -3
- data/lib/rager/{mesh_gen → mesh}/providers/abstract.rb +4 -4
- data/lib/rager/{mesh_gen → mesh}/providers/replicate.rb +13 -9
- data/lib/rager/operation.rb +2 -2
- data/lib/rager/options.rb +1 -1
- data/lib/rager/outcome.rb +25 -0
- data/lib/rager/providers.rb +37 -25
- data/lib/rager/rerank/input.rb +20 -0
- data/lib/rager/rerank/options.rb +2 -2
- data/lib/rager/rerank/providers/abstract.rb +2 -3
- data/lib/rager/rerank/providers/cohere.rb +19 -13
- data/lib/rager/rerank/result.rb +20 -0
- data/lib/rager/result.rb +92 -122
- data/lib/rager/search/options.rb +3 -2
- data/lib/rager/search/providers/jina.rb +16 -15
- data/lib/rager/search/result.rb +21 -0
- data/lib/rager/template/input.rb +20 -0
- data/lib/rager/template/options.rb +1 -1
- data/lib/rager/template/providers/abstract.rb +2 -3
- data/lib/rager/template/providers/erb.rb +4 -5
- data/lib/rager/template/providers/mustache.rb +30 -0
- data/lib/rager/types.rb +31 -24
- data/lib/rager/utils/http.rb +73 -15
- data/lib/rager/utils/replicate.rb +28 -6
- data/lib/rager/utils/runtime.rb +21 -0
- data/lib/rager/version.rb +1 -1
- metadata +18 -12
- data/lib/rager/rerank/output.rb +0 -13
- data/lib/rager/search/output.rb +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2db5c90419613081d3292daff1f7d16ef7a5c9e5b459dfcd3e78226ebc86538b
|
4
|
+
data.tar.gz: e43d6a988df3da33390e3b278b77eb44967aad6f667c2c4b604ee61fab30261f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 490172cb198dfd79c391b56fadebc44124f766334b19cc9c79997c08f82351a71834e0a647e758ed63fd2ff0a4837da61527e8ee19b451a258f9e66b44b334b3
|
7
|
+
data.tar.gz: a37fe08071870f6dd9cce48db5f45dd3af0ae2e42e1aaeabc3bd12deac2d6746e6108035f9e64c6be12f39bdd0a8ee35c57dfe8f4a679c84b157e65887664508
|
data/README.md
CHANGED
@@ -8,41 +8,41 @@
|
|
8
8
|
|
9
9
|
Build continuously improving generative workflows.
|
10
10
|
|
11
|
-
##
|
11
|
+
## Logging
|
12
12
|
|
13
|
-
|
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) |
|
13
|
+
Why yet another library? The main reason is to have an out-of-the-box logging server that records workflows and outcomes to use in few-shot prompts and fine-tuning. It’s live at [`rager.cloud`](https://rager.cloud), though public signups are paused until I roll out usage-based billing.
|
22
14
|
|
23
|
-
##
|
15
|
+
## Providers
|
24
16
|
|
25
|
-
|
17
|
+
Modalities that take a URL parameter (chat, embedding, rerank) support compatible services, allowing you to use alternative providers that support the interface.
|
18
|
+
|
19
|
+
| Feature | Providers |
|
20
|
+
| ---------------------- | ---------------- |
|
21
|
+
| **Chat** | `openai` |
|
22
|
+
| **Embedding** | `openai` |
|
23
|
+
| **Image generation** | `replicate` |
|
24
|
+
| **3D mesh generation** | `replicate` |
|
25
|
+
| **Reranking** | `cohere` |
|
26
|
+
| **Search** | `jina` |
|
27
|
+
| **Templating** | `erb`,`mustache` |
|
26
28
|
|
27
29
|
## Installation
|
28
30
|
|
29
|
-
Add this line to your application
|
31
|
+
Add this line to your application's Gemfile:
|
30
32
|
|
31
33
|
```ruby
|
32
|
-
gem "rager", "~> 0.
|
34
|
+
gem "rager", "~> 0.8.0"
|
33
35
|
```
|
34
36
|
|
35
|
-
Or use it in a standalone script (
|
37
|
+
Or use it in a standalone script (requires OPENAI_API_KEY set):
|
36
38
|
|
37
39
|
```ruby
|
38
|
-
#!/usr/bin/env ruby
|
39
|
-
|
40
40
|
require "bundler/inline"
|
41
41
|
|
42
42
|
gemfile do
|
43
43
|
source "https://rubygems.org"
|
44
44
|
gem "async-http", "~> 0.88.0"
|
45
|
-
gem "rager", "~> 0.
|
45
|
+
gem "rager", "~> 0.8.0"
|
46
46
|
end
|
47
47
|
|
48
48
|
require "rager"
|
@@ -51,19 +51,16 @@ Rager.configure do |config|
|
|
51
51
|
config.http_adapter = Rager::Http::Adapters::AsyncHttp.new
|
52
52
|
end
|
53
53
|
|
54
|
+
PROMPT = ARGV[0] || "Ruby programming"
|
55
|
+
|
54
56
|
Async do
|
55
57
|
ctx = Rager::Context.new
|
56
|
-
prompt = ctx.template(
|
57
|
-
"Tell me about the history of <%= topic %>:\n",
|
58
|
-
{topic: "Ruby programming"}
|
59
|
-
)
|
58
|
+
prompt = ctx.template("Tell me about the history of <%= topic %>", {topic: PROMPT})
|
60
59
|
chat = ctx.chat(prompt, stream: true)
|
61
|
-
chat.
|
60
|
+
chat.stream.each { |d| print d.content }
|
62
61
|
end
|
63
62
|
```
|
64
63
|
|
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
|
-
|
67
64
|
## License
|
68
65
|
|
69
66
|
[MIT](./LICENSE.md)
|
data/lib/rager/chat/message.rb
CHANGED
@@ -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
|
data/lib/rager/chat/options.rb
CHANGED
@@ -10,22 +10,21 @@ module Rager
|
|
10
10
|
extend T::Sig
|
11
11
|
include Rager::Options
|
12
12
|
|
13
|
-
const :provider,
|
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[
|
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
|
-
|
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.
|
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:
|
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,
|
83
|
-
|
84
|
+
def build_openai_messages(messages, system_prompt)
|
85
|
+
output = T.let([], OpenaiMessages)
|
84
86
|
|
85
|
-
if
|
86
|
-
|
87
|
+
if system_prompt
|
88
|
+
output << {"role" => "system", "content" => system_prompt}
|
87
89
|
end
|
88
90
|
|
89
|
-
|
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
|
-
|
97
|
+
output << {"role" => role, "content" => content}
|
96
98
|
when Array
|
97
99
|
formatted_content = content.map { |item| format_content_item(item) }
|
98
|
-
|
100
|
+
output << {"role" => role, "content" => formatted_content}
|
99
101
|
end
|
100
102
|
end
|
101
103
|
|
102
|
-
|
104
|
+
output
|
103
105
|
end
|
104
106
|
|
105
|
-
sig { params(item: Rager::Chat::MessageContent).returns(
|
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::
|
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
|
-
|
142
|
+
buffer_parts = []
|
141
143
|
body.each do |chunk|
|
142
|
-
|
144
|
+
buffer_parts << chunk.force_encoding(Encoding::UTF_8)
|
145
|
+
buffer = buffer_parts.join
|
143
146
|
while (line = buffer.slice!(/.*\n/))
|
144
|
-
line
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
data/lib/rager/chat/schema.rb
CHANGED
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
|