rager 0.1.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 +7 -0
- data/LICENSE.md +21 -0
- data/README.md +23 -0
- data/lib/rager/chat/message.rb +13 -0
- data/lib/rager/chat/message_content.rb +14 -0
- data/lib/rager/chat/message_content_image_type.rb +16 -0
- data/lib/rager/chat/message_content_type.rb +16 -0
- data/lib/rager/chat/message_delta.rb +13 -0
- data/lib/rager/chat/message_role.rb +16 -0
- data/lib/rager/chat/options.rb +52 -0
- data/lib/rager/chat/providers/abstract.rb +25 -0
- data/lib/rager/chat/providers/openai.rb +196 -0
- data/lib/rager/chat/schema.rb +48 -0
- data/lib/rager/chat.rb +35 -0
- data/lib/rager/config.rb +30 -0
- data/lib/rager/context.rb +116 -0
- data/lib/rager/error.rb +7 -0
- data/lib/rager/errors/http_error.rb +19 -0
- data/lib/rager/errors/missing_credentials_error.rb +19 -0
- data/lib/rager/errors/options_error.rb +19 -0
- data/lib/rager/errors/parse_error.rb +19 -0
- data/lib/rager/errors/unknown_provider_error.rb +17 -0
- data/lib/rager/http/adapters/abstract.rb +20 -0
- data/lib/rager/http/adapters/async_http.rb +65 -0
- data/lib/rager/http/adapters/mock.rb +138 -0
- data/lib/rager/http/request.rb +15 -0
- data/lib/rager/http/response.rb +14 -0
- data/lib/rager/http/verb.rb +20 -0
- data/lib/rager/image_gen/options.rb +29 -0
- data/lib/rager/image_gen/providers/abstract.rb +25 -0
- data/lib/rager/image_gen/providers/replicate.rb +55 -0
- data/lib/rager/image_gen.rb +31 -0
- data/lib/rager/logger.rb +13 -0
- data/lib/rager/mesh_gen/options.rb +30 -0
- data/lib/rager/mesh_gen/providers/abstract.rb +25 -0
- data/lib/rager/mesh_gen/providers/replicate.rb +61 -0
- data/lib/rager/mesh_gen.rb +31 -0
- data/lib/rager/operation.rb +14 -0
- data/lib/rager/options.rb +21 -0
- data/lib/rager/result.rb +198 -0
- data/lib/rager/types.rb +45 -0
- data/lib/rager/version.rb +6 -0
- data/lib/rager.rb +35 -0
- metadata +123 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5b4311f3da8c6cf4e479c8c951322e329b4f4d697a8f78c6b2945b89793a2903
|
4
|
+
data.tar.gz: b92e1be42bcf7a769da8d1164f0dcd0d3108249bf3e1266cf1f2b0612bdab69f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7bb128546a71dc3e61e28d7c7ffbbfaa7dfdbf1f3b5236de89eb7c9069cb94d45ca5b3336650ee4e7d29a9cfbb8b4a88ead87134ff0b781aba08c3c90ce5b00d
|
7
|
+
data.tar.gz: bcdb5dbd47c1fc6f7014e321349aa95102c6cd1492872755afe5ca32101d7cd9aa68b24c4eb98021c2cd19edc44479121c01f5ce118f4b539ffc39a5979019cb
|
data/LICENSE.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Marko Vukovic
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# rager_rb
|
2
|
+
|
3
|
+
[](https://github.com/mvkvc/rager_rb/actions/workflows/test.yaml)
|
4
|
+
|
5
|
+
Build continuously improving AI applications.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
If you are using Bundler:
|
10
|
+
|
11
|
+
```bash
|
12
|
+
bundle add rager
|
13
|
+
```
|
14
|
+
|
15
|
+
Otherwise you can add it to your Gemfile directly:
|
16
|
+
|
17
|
+
```Ruby
|
18
|
+
gem "rager", "~> 0.1.0"
|
19
|
+
```
|
20
|
+
|
21
|
+
## License
|
22
|
+
|
23
|
+
[MIT](./LICENSE.md)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
|
6
|
+
module Rager
|
7
|
+
module Chat
|
8
|
+
class MessageContent < T::Struct
|
9
|
+
const :type, MessageContentType, default: MessageContentType::Text
|
10
|
+
const :image_type, T.nilable(MessageContentImageType)
|
11
|
+
const :content, String
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
|
6
|
+
module Rager
|
7
|
+
module Chat
|
8
|
+
class MessageContentImageType < T::Enum
|
9
|
+
enums do
|
10
|
+
Jpeg = new("jpeg")
|
11
|
+
Png = new("png")
|
12
|
+
Webp = new("webp")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
|
6
|
+
module Rager
|
7
|
+
module Chat
|
8
|
+
class MessageContentType < T::Enum
|
9
|
+
enums do
|
10
|
+
Text = new("text")
|
11
|
+
ImageUrl = new("image_url")
|
12
|
+
ImageBase64 = new("image_base64")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
|
6
|
+
module Rager
|
7
|
+
module Chat
|
8
|
+
class MessageRole < T::Enum
|
9
|
+
enums do
|
10
|
+
User = new("user")
|
11
|
+
Assistant = new("assistant")
|
12
|
+
System = new("system")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "dry-schema"
|
5
|
+
require "sorbet-runtime"
|
6
|
+
|
7
|
+
module Rager
|
8
|
+
module Chat
|
9
|
+
class Options < T::Struct
|
10
|
+
extend T::Sig
|
11
|
+
include Rager::Options
|
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)
|
17
|
+
const :model, T.nilable(String)
|
18
|
+
const :stream, T.nilable(T::Boolean)
|
19
|
+
const :n, T.nilable(Integer)
|
20
|
+
const :temperature, T.nilable(Float)
|
21
|
+
const :system_prompt, T.nilable(String)
|
22
|
+
const :schema, T.nilable(Dry::Schema::JSON)
|
23
|
+
const :schema_name, T.nilable(String)
|
24
|
+
const :seed, T.nilable(Integer)
|
25
|
+
|
26
|
+
sig { override.returns(T::Hash[String, T.untyped]) }
|
27
|
+
def serialize_safe
|
28
|
+
result = serialize
|
29
|
+
result["api_key"] = "[REDACTED]" if result.key?("api_key")
|
30
|
+
result["schema"] = Rager::Chat::Schema.dry_schema_to_json_schema(result["schema"]) if result.key?("schema")
|
31
|
+
result
|
32
|
+
end
|
33
|
+
|
34
|
+
sig { override.void }
|
35
|
+
def validate
|
36
|
+
if stream && schema
|
37
|
+
raise Rager::Errors::OptionsError.new(
|
38
|
+
invalid_keys: %w[stream schema],
|
39
|
+
description: "You cannot use streaming with structured outputs"
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
if schema && schema_name.nil?
|
44
|
+
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
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
|
6
|
+
module Rager
|
7
|
+
module Chat
|
8
|
+
module Providers
|
9
|
+
class Abstract
|
10
|
+
extend T::Sig
|
11
|
+
extend T::Helpers
|
12
|
+
abstract!
|
13
|
+
|
14
|
+
sig do
|
15
|
+
abstract.params(
|
16
|
+
messages: T::Array[Rager::Chat::Message],
|
17
|
+
options: Rager::Chat::Options
|
18
|
+
).returns(Rager::Types::ChatOutput)
|
19
|
+
end
|
20
|
+
def chat(messages, options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "json"
|
5
|
+
require "sorbet-runtime"
|
6
|
+
|
7
|
+
module Rager
|
8
|
+
module Chat
|
9
|
+
module Providers
|
10
|
+
class Openai < Rager::Chat::Providers::Abstract
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
OpenaiMessages = T.type_alias { T::Array[T::Hash[String, T.any(String, T::Array[T.untyped])]] }
|
14
|
+
|
15
|
+
sig do
|
16
|
+
override.params(
|
17
|
+
messages: T::Array[Rager::Chat::Message],
|
18
|
+
options: Rager::Chat::Options
|
19
|
+
).returns(Rager::Types::ChatOutput)
|
20
|
+
end
|
21
|
+
def chat(messages, options)
|
22
|
+
api_key = options.api_key || ENV["OPENAI_API_KEY"]
|
23
|
+
raise Rager::Errors::MissingCredentialsError.new("OpenAI", "OPENAI_API_KEY") if api_key.nil?
|
24
|
+
|
25
|
+
url = options.url || ENV["OPENAI_URL"] || "https://api.openai.com/v1/chat/completions"
|
26
|
+
model = options.model || "gpt-4o"
|
27
|
+
|
28
|
+
openai_messages = build_openai_messages(messages, options.history, options.system_prompt)
|
29
|
+
|
30
|
+
headers = {
|
31
|
+
"Content-Type" => "application/json"
|
32
|
+
}
|
33
|
+
headers["Authorization"] = "Bearer #{api_key}" if api_key
|
34
|
+
|
35
|
+
body = {
|
36
|
+
model: model,
|
37
|
+
messages: openai_messages
|
38
|
+
}
|
39
|
+
body[:temperature] = options.temperature unless options.temperature.nil?
|
40
|
+
body[:n] = options.n unless options.n.nil?
|
41
|
+
body[:stream] = options.stream unless options.stream.nil?
|
42
|
+
body[:seed] = options.seed unless options.seed.nil?
|
43
|
+
|
44
|
+
if options.schema && options.schema_name
|
45
|
+
body[:response_format] = {
|
46
|
+
type: "json_schema",
|
47
|
+
json_schema: {
|
48
|
+
name: T.must(options.schema_name).downcase,
|
49
|
+
strict: true,
|
50
|
+
schema: Rager::Chat::Schema.dry_schema_to_json_schema(T.must(options.schema))
|
51
|
+
}
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
request = Rager::Http::Request.new(
|
56
|
+
verb: Rager::Http::Verb::Post,
|
57
|
+
url: url,
|
58
|
+
headers: headers,
|
59
|
+
body: body.to_json
|
60
|
+
)
|
61
|
+
|
62
|
+
http_adapter = Rager.config.http_adapter
|
63
|
+
response = http_adapter.make_request(request)
|
64
|
+
response_body = T.must(response.body)
|
65
|
+
|
66
|
+
if response.status != 200
|
67
|
+
raise Rager::Errors::HttpError.new(
|
68
|
+
http_adapter,
|
69
|
+
response.status,
|
70
|
+
T.cast(response_body, String)
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
case response_body
|
75
|
+
when String
|
76
|
+
parse_non_stream_body(response_body)
|
77
|
+
when Enumerator
|
78
|
+
create_message_delta_stream(response_body)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
sig do
|
83
|
+
params(
|
84
|
+
messages: T::Array[Rager::Chat::Message],
|
85
|
+
history: T::Array[Rager::Chat::Message],
|
86
|
+
system_prompt: T.nilable(String)
|
87
|
+
).returns(OpenaiMessages)
|
88
|
+
end
|
89
|
+
def build_openai_messages(messages, history, system_prompt)
|
90
|
+
result = T.let([], OpenaiMessages)
|
91
|
+
|
92
|
+
if history.empty? && system_prompt && !system_prompt.empty?
|
93
|
+
result << {"role" => "system",
|
94
|
+
"content" => system_prompt}
|
95
|
+
end
|
96
|
+
|
97
|
+
history.each do |msg|
|
98
|
+
role_str = msg.role.is_a?(String) ? msg.role : msg.role.serialize
|
99
|
+
result << {"role" => role_str, "content" => msg.content}
|
100
|
+
end
|
101
|
+
|
102
|
+
messages.each do |message|
|
103
|
+
role_str = message.role.is_a?(String) ? message.role : message.role.serialize
|
104
|
+
content = message.content
|
105
|
+
|
106
|
+
if content.is_a?(String)
|
107
|
+
result << {"role" => role_str, "content" => content}
|
108
|
+
elsif content.is_a?(Array)
|
109
|
+
formatted_content = content.map do |item|
|
110
|
+
item_type = item.type
|
111
|
+
case item_type
|
112
|
+
when Rager::Chat::MessageContentType::Text
|
113
|
+
{"type" => "text", "text" => item.content}
|
114
|
+
when Rager::Chat::MessageContentType::ImageUrl
|
115
|
+
{"type" => "image_url", "image_url" => {"url" => item.content}}
|
116
|
+
when Rager::Chat::MessageContentType::ImageBase64
|
117
|
+
image_type = T.must(item.image_type)
|
118
|
+
image_mime_type = case image_type
|
119
|
+
when Rager::Chat::MessageContentImageType::Jpeg then "image/jpeg"
|
120
|
+
when Rager::Chat::MessageContentImageType::Png then "image/png"
|
121
|
+
when Rager::Chat::MessageContentImageType::Webp then "image/webp"
|
122
|
+
end
|
123
|
+
data_uri = "data:#{image_mime_type};base64,#{item.content}"
|
124
|
+
{"type" => "image_url", "image_url" => {"url" => data_uri}}
|
125
|
+
end
|
126
|
+
end
|
127
|
+
result << {"role" => role_str, "content" => formatted_content}
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
result
|
132
|
+
end
|
133
|
+
|
134
|
+
sig { params(body: String).returns(T::Array[String]) }
|
135
|
+
def parse_non_stream_body(body)
|
136
|
+
messages = T.let([], T::Array[String])
|
137
|
+
|
138
|
+
begin
|
139
|
+
response_data = JSON.parse(body)
|
140
|
+
if response_data.key?("choices") && response_data["choices"].is_a?(Array)
|
141
|
+
response_data["choices"].each do |choice|
|
142
|
+
text = choice.dig("message", "content").to_s
|
143
|
+
messages << text unless text.empty?
|
144
|
+
end
|
145
|
+
end
|
146
|
+
rescue JSON::ParserError
|
147
|
+
raise Rager::Errors::ParseError.new(
|
148
|
+
"OpenAI response body is not valid JSON",
|
149
|
+
body
|
150
|
+
)
|
151
|
+
end
|
152
|
+
|
153
|
+
messages
|
154
|
+
end
|
155
|
+
|
156
|
+
sig { params(body: T::Enumerator[String]).returns(T::Enumerator[Rager::Chat::MessageDelta]) }
|
157
|
+
def create_message_delta_stream(body)
|
158
|
+
Enumerator.new do |yielder|
|
159
|
+
buffer = +""
|
160
|
+
|
161
|
+
process_chunk = lambda do |chunk|
|
162
|
+
buffer << chunk
|
163
|
+
pattern = /\Adata: (.*?)\n\n|\Adata: (.*?)\n/
|
164
|
+
while (event_match = buffer.match(pattern))
|
165
|
+
full_event = T.must(event_match[0])
|
166
|
+
data_line = event_match[1] || event_match[2]
|
167
|
+
|
168
|
+
buffer.delete_prefix!(full_event)
|
169
|
+
|
170
|
+
next if data_line.nil? || data_line.strip.empty?
|
171
|
+
next if data_line.strip == "[DONE]"
|
172
|
+
|
173
|
+
begin
|
174
|
+
data = JSON.parse(data_line)
|
175
|
+
if data.key?("choices") && data["choices"].is_a?(Array)
|
176
|
+
data["choices"].each do |choice|
|
177
|
+
choice_index = choice.dig("index") || 0
|
178
|
+
delta = choice.dig("delta", "content")
|
179
|
+
yielder << Rager::Chat::MessageDelta.new(index: choice_index, content: delta) if delta
|
180
|
+
end
|
181
|
+
end
|
182
|
+
rescue JSON::ParserError
|
183
|
+
next
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
body.each(&process_chunk)
|
189
|
+
|
190
|
+
process_chunk.call("\n") unless buffer.empty?
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "dry-schema"
|
5
|
+
require "json"
|
6
|
+
require "sorbet-runtime"
|
7
|
+
|
8
|
+
Dry::Schema.load_extensions(:json_schema)
|
9
|
+
|
10
|
+
module Rager
|
11
|
+
module Chat
|
12
|
+
module Schema
|
13
|
+
extend T::Sig
|
14
|
+
|
15
|
+
sig { params(schema: Dry::Schema::JSON).returns(T::Hash[Symbol, T.untyped]) }
|
16
|
+
def self.dry_schema_to_json_schema(schema)
|
17
|
+
json_schema_original = schema.json_schema
|
18
|
+
json_schema = JSON.parse(JSON.generate(json_schema_original).force_encoding("UTF-8"))
|
19
|
+
|
20
|
+
make_strict_recursive!(json_schema)
|
21
|
+
|
22
|
+
json_schema
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { params(node: T.untyped).void }
|
26
|
+
def self.make_strict_recursive!(node)
|
27
|
+
case node
|
28
|
+
when Hash
|
29
|
+
%w[minLength maxLength not].each { |key| node.delete(key) }
|
30
|
+
|
31
|
+
case node["type"]
|
32
|
+
when "object"
|
33
|
+
node["additionalProperties"] = false
|
34
|
+
make_strict_recursive!(node["properties"]) if node.key?("properties")
|
35
|
+
when "array"
|
36
|
+
make_strict_recursive!(node["items"]) if node.key?("items")
|
37
|
+
end
|
38
|
+
|
39
|
+
node.each_value { |v| make_strict_recursive!(v) }
|
40
|
+
when Array
|
41
|
+
node.each { |item| make_strict_recursive!(item) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private_class_method :make_strict_recursive!
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/rager/chat.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
|
6
|
+
module Rager
|
7
|
+
module Chat
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig do
|
11
|
+
params(
|
12
|
+
messages: T::Array[Rager::Chat::Message],
|
13
|
+
options: Rager::Chat::Options
|
14
|
+
).returns(Rager::Types::ChatOutput)
|
15
|
+
end
|
16
|
+
def self.chat(messages, options = Rager::Chat::Options.new)
|
17
|
+
provider = get_provider(options.provider)
|
18
|
+
provider.chat(messages, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
sig do
|
22
|
+
params(
|
23
|
+
key: String
|
24
|
+
).returns(Rager::Chat::Providers::Abstract)
|
25
|
+
end
|
26
|
+
def self.get_provider(key)
|
27
|
+
case key.downcase
|
28
|
+
when "openai"
|
29
|
+
Rager::Chat::Providers::Openai.new
|
30
|
+
else
|
31
|
+
raise Rager::Errors::UnknownProviderError.new(Rager::Operation::Chat, key)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/rager/config.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
|
6
|
+
module Rager
|
7
|
+
class Config
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig { returns(Rager::Http::Adapters::Abstract) }
|
11
|
+
attr_accessor :http_adapter
|
12
|
+
|
13
|
+
sig { returns(T.nilable(Rager::Logger)) }
|
14
|
+
attr_accessor :logger
|
15
|
+
|
16
|
+
sig { returns(T.nilable(String)) }
|
17
|
+
attr_accessor :url
|
18
|
+
|
19
|
+
sig { returns(T.nilable(String)) }
|
20
|
+
attr_accessor :api_key
|
21
|
+
|
22
|
+
sig { void }
|
23
|
+
def initialize
|
24
|
+
@http_adapter = T.let(Rager::Http::Adapters::AsyncHttp.new, Rager::Http::Adapters::Abstract)
|
25
|
+
@logger = T.let(nil, T.nilable(Rager::Logger))
|
26
|
+
@url = T.let(nil, T.nilable(String))
|
27
|
+
@api_key = T.let(nil, T.nilable(String))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "securerandom"
|
5
|
+
require "sorbet-runtime"
|
6
|
+
|
7
|
+
module Rager
|
8
|
+
class Context
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { returns(String) }
|
12
|
+
attr_reader :id
|
13
|
+
|
14
|
+
sig { returns(T.nilable(String)) }
|
15
|
+
attr_reader :hash
|
16
|
+
|
17
|
+
sig { params(id: T.nilable(String)).void }
|
18
|
+
def initialize(id: nil)
|
19
|
+
@id = T.let(id || SecureRandom.uuid, String)
|
20
|
+
@hash = T.let(lookup_git_hash, T.nilable(String))
|
21
|
+
end
|
22
|
+
|
23
|
+
sig do
|
24
|
+
params(
|
25
|
+
messages: T.any(String, T::Array[Rager::Chat::Message]),
|
26
|
+
kwargs: T.untyped
|
27
|
+
).returns(Rager::Result)
|
28
|
+
end
|
29
|
+
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
|
+
execute(
|
40
|
+
Rager::Operation::Chat,
|
41
|
+
Rager::Chat::Options,
|
42
|
+
kwargs,
|
43
|
+
messages
|
44
|
+
) { |options| Chat.chat(messages, options) }
|
45
|
+
end
|
46
|
+
|
47
|
+
sig do
|
48
|
+
params(
|
49
|
+
prompt: String,
|
50
|
+
kwargs: T.untyped
|
51
|
+
).returns(Rager::Result)
|
52
|
+
end
|
53
|
+
def image_gen(prompt, **kwargs)
|
54
|
+
execute(
|
55
|
+
Rager::Operation::ImageGen,
|
56
|
+
Rager::ImageGen::Options,
|
57
|
+
kwargs,
|
58
|
+
prompt
|
59
|
+
) { |options| ImageGen.image_gen(prompt, options) }
|
60
|
+
end
|
61
|
+
|
62
|
+
sig do
|
63
|
+
params(
|
64
|
+
prompt: String,
|
65
|
+
kwargs: T.untyped
|
66
|
+
).returns(Rager::Result)
|
67
|
+
end
|
68
|
+
def mesh_gen(prompt, **kwargs)
|
69
|
+
execute(
|
70
|
+
Rager::Operation::MeshGen,
|
71
|
+
Rager::MeshGen::Options,
|
72
|
+
kwargs,
|
73
|
+
prompt
|
74
|
+
) { |options| MeshGen.mesh_gen(prompt, options) }
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
sig do
|
80
|
+
params(
|
81
|
+
operation: Rager::Operation,
|
82
|
+
options_struct: T::Class[Rager::Options],
|
83
|
+
kwargs: T.untyped,
|
84
|
+
input: T.any(String, T::Array[Rager::Chat::Message]),
|
85
|
+
block: T.proc.params(options: T.untyped).returns(T.untyped)
|
86
|
+
).returns(Rager::Result)
|
87
|
+
end
|
88
|
+
def execute(operation, options_struct, kwargs, input, &block)
|
89
|
+
options = options_struct.new(**kwargs)
|
90
|
+
options.validate
|
91
|
+
|
92
|
+
start_time = Time.now
|
93
|
+
|
94
|
+
output = yield(options)
|
95
|
+
|
96
|
+
Result.new(
|
97
|
+
context_id: @id,
|
98
|
+
hash: @hash,
|
99
|
+
operation: operation,
|
100
|
+
input: input,
|
101
|
+
options: options,
|
102
|
+
start_time: start_time.to_i,
|
103
|
+
end_time: Time.now.to_i,
|
104
|
+
output: output
|
105
|
+
).tap { |r| r.log }
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
sig { returns(T.nilable(String)) }
|
111
|
+
def lookup_git_hash
|
112
|
+
result = `git rev-parse HEAD`
|
113
|
+
$?.success? ? result.strip : nil
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/rager/error.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
|
6
|
+
module Rager
|
7
|
+
module Errors
|
8
|
+
class HttpError < Rager::Error
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { params(adapter: Rager::Http::Adapters::Abstract, status: Integer, body: T.nilable(String)).void }
|
12
|
+
def initialize(adapter, status, body)
|
13
|
+
message = "HTTP Error #{status} using adapter #{adapter.class.name}"
|
14
|
+
message += " -- #{body}" if body
|
15
|
+
super(message)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|