rach 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/README.md +12 -0
- data/lib/rach/client.rb +44 -0
- data/lib/rach/conversation.rb +49 -0
- data/lib/rach/errors.rb +4 -0
- data/lib/rach/function.rb +84 -0
- data/lib/rach/message.rb +17 -0
- data/lib/rach/message_template.rb +21 -0
- data/lib/rach/response.rb +61 -0
- data/lib/rach/response_format.rb +57 -0
- data/lib/rach/usage_tracker.rb +35 -0
- data/lib/rach/version.rb +3 -0
- data/lib/rach.rb +16 -0
- metadata +117 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7f427f0e11f94e869ea14d6d7ed35bd5d73311212fd958daa52b1d8964886fc3
|
4
|
+
data.tar.gz: b0fbf4e2dfcc18c9aa39c6e23b8ebc5edafa62deb125a4b0d25bb0c0eeb5ba4c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0abf7aa58e283fa66cf18b322c9711f250365760b6f77b25d1276e77daf28a0f0640f98050b0bc2ccee38b181735398572e21c95d3f0872a770e6f545ce40cd5
|
7
|
+
data.tar.gz: 2a2553158981ff4f425ada8381c3e61635f302faf5b88c9d952247a60ea126dda4ddc91440dd32324194a94a80d86aad283d0a74bc2a1b3c48066348447f96ae
|
data/README.md
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# Rach
|
2
|
+
|
3
|
+
A lightweight Ruby framework for OpenAI interactions, focusing on simplicity and clean design.
|
4
|
+
|
5
|
+
|
6
|
+
## Configuration
|
7
|
+
|
8
|
+
1. Copy the example environment file:
|
9
|
+
```
|
10
|
+
cp .env.example .env
|
11
|
+
```
|
12
|
+
2. Set your OpenAI API key in the `.env` file.
|
data/lib/rach/client.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module Rach
|
2
|
+
class Client
|
3
|
+
attr_reader :tracker, :client, :model
|
4
|
+
|
5
|
+
def initialize(access_token:, model: "gpt-4o-mini")
|
6
|
+
@client = OpenAI::Client.new(log_errors: true, access_token: access_token)
|
7
|
+
@model = model
|
8
|
+
@tracker = UsageTracker.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def chat(prompt, response_format: nil, tools: nil)
|
12
|
+
messages = format_messages(prompt)
|
13
|
+
|
14
|
+
response = Response.new(
|
15
|
+
@client.chat(
|
16
|
+
parameters: {
|
17
|
+
model: @model,
|
18
|
+
messages:,
|
19
|
+
response_format:,
|
20
|
+
tools:,
|
21
|
+
}.compact
|
22
|
+
)
|
23
|
+
)
|
24
|
+
|
25
|
+
@tracker.track(response)
|
26
|
+
response
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def format_messages(prompt)
|
32
|
+
case prompt
|
33
|
+
when String
|
34
|
+
[{ role: "user", content: prompt }]
|
35
|
+
when Message
|
36
|
+
[prompt.to_h]
|
37
|
+
when Conversation
|
38
|
+
prompt.to_a
|
39
|
+
else
|
40
|
+
raise ArgumentError, "prompt must be a String, Message, or Conversation"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Rach
|
2
|
+
class Conversation
|
3
|
+
def initialize
|
4
|
+
@messages = []
|
5
|
+
end
|
6
|
+
|
7
|
+
def add_message(message)
|
8
|
+
raise ArgumentError, "Expected Message object" unless message.is_a?(Message)
|
9
|
+
@messages << message
|
10
|
+
self
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_response(response)
|
14
|
+
assistant(response.content)
|
15
|
+
end
|
16
|
+
|
17
|
+
def add(content, role: "user")
|
18
|
+
add_message(Message.new(content: content, role: role))
|
19
|
+
end
|
20
|
+
|
21
|
+
def system(content)
|
22
|
+
add(content, role: "system")
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def user(content)
|
27
|
+
add(content, role: "user")
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def assistant(content)
|
32
|
+
add(content, role: "assistant")
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_a
|
37
|
+
@messages.map(&:to_h)
|
38
|
+
end
|
39
|
+
|
40
|
+
def clear
|
41
|
+
@messages.clear
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def empty?
|
46
|
+
@messages.empty?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/rach/errors.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
module Rach
|
2
|
+
module Function
|
3
|
+
def self.included(base)
|
4
|
+
require 'json/schema_builder'
|
5
|
+
base.include JSON::SchemaBuilder
|
6
|
+
base.extend ClassMethods
|
7
|
+
|
8
|
+
# Register the function when included
|
9
|
+
functions << base
|
10
|
+
end
|
11
|
+
|
12
|
+
# Add function registry
|
13
|
+
def self.functions
|
14
|
+
@functions ||= []
|
15
|
+
end
|
16
|
+
|
17
|
+
# Add function lookup method
|
18
|
+
def self.find_by_name(name)
|
19
|
+
functions.find { |func| func.new.function_name == name }
|
20
|
+
end
|
21
|
+
|
22
|
+
module ClassMethods
|
23
|
+
def function_schema
|
24
|
+
schema = new.schema
|
25
|
+
{
|
26
|
+
type: "function",
|
27
|
+
function: {
|
28
|
+
name: new.function_name,
|
29
|
+
description: new.function_description,
|
30
|
+
parameters: prepare_schema_for_function(schema)
|
31
|
+
}
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def prepare_schema_for_function(schema)
|
38
|
+
schema.additional_properties(false)
|
39
|
+
schema_hash = schema.as_json.deep_symbolize_keys
|
40
|
+
|
41
|
+
# Set required fields and additional properties
|
42
|
+
deep_set_object_properties(schema_hash)
|
43
|
+
|
44
|
+
schema_hash
|
45
|
+
end
|
46
|
+
|
47
|
+
def deep_set_object_properties(schema_hash)
|
48
|
+
return unless schema_hash.is_a?(Hash)
|
49
|
+
|
50
|
+
if schema_hash[:type] == "object"
|
51
|
+
schema_hash[:additionalProperties] = false
|
52
|
+
if schema_hash[:properties]
|
53
|
+
schema_hash[:required] = schema_hash[:properties].keys
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
schema_hash.each_value do |value|
|
58
|
+
if value.is_a?(Hash)
|
59
|
+
deep_set_object_properties(value)
|
60
|
+
elsif value.is_a?(Array)
|
61
|
+
value.each { |item| deep_set_object_properties(item) }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Instance methods that must be implemented by including class
|
68
|
+
def schema
|
69
|
+
raise NotImplementedError, "#{self.class} must implement #schema"
|
70
|
+
end
|
71
|
+
|
72
|
+
def execute(**args)
|
73
|
+
raise NotImplementedError, "#{self.class} must implement #execute"
|
74
|
+
end
|
75
|
+
|
76
|
+
def function_name
|
77
|
+
raise NotImplementedError, "#{self.class} must implement #function_name"
|
78
|
+
end
|
79
|
+
|
80
|
+
def function_description
|
81
|
+
raise NotImplementedError, "#{self.class} must implement #function_description"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/rach/message.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Rach
|
2
|
+
class MessageTemplate
|
3
|
+
def initialize(template, role: "user")
|
4
|
+
@template = template
|
5
|
+
@role = role
|
6
|
+
end
|
7
|
+
|
8
|
+
def render(variables = {})
|
9
|
+
interpolated = interpolate(@template, variables)
|
10
|
+
Message.new(content: interpolated, role: @role)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def interpolate(text, variables)
|
16
|
+
variables.reduce(text) do |result, (key, value)|
|
17
|
+
result.gsub(/\{\{#{key}\}\}/, value.to_s)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Rach
|
2
|
+
class Response
|
3
|
+
attr_reader :raw_response
|
4
|
+
|
5
|
+
def initialize(response)
|
6
|
+
@raw_response = response
|
7
|
+
end
|
8
|
+
|
9
|
+
def content
|
10
|
+
message&.dig("content")
|
11
|
+
end
|
12
|
+
|
13
|
+
def tool_calls
|
14
|
+
message&.dig("tool_calls")
|
15
|
+
end
|
16
|
+
|
17
|
+
def function_call?
|
18
|
+
!tool_calls.nil? && !tool_calls.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
def function_name
|
22
|
+
return nil unless function_call?
|
23
|
+
tool_calls.first.dig("function", "name")
|
24
|
+
end
|
25
|
+
|
26
|
+
def function_arguments
|
27
|
+
return nil unless function_call?
|
28
|
+
JSON.parse(tool_calls.first.dig("function", "arguments"))
|
29
|
+
rescue JSON::ParserError
|
30
|
+
raise ParseError, "Function arguments are not valid JSON"
|
31
|
+
end
|
32
|
+
|
33
|
+
def usage
|
34
|
+
@raw_response["usage"]
|
35
|
+
end
|
36
|
+
|
37
|
+
def prompt_tokens
|
38
|
+
usage&.fetch("prompt_tokens", 0)
|
39
|
+
end
|
40
|
+
|
41
|
+
def completion_tokens
|
42
|
+
usage&.fetch("completion_tokens", 0)
|
43
|
+
end
|
44
|
+
|
45
|
+
def total_tokens
|
46
|
+
usage&.fetch("total_tokens", 0)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def message
|
52
|
+
@raw_response.dig("choices", 0, "message")
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_json
|
56
|
+
JSON.parse(content)
|
57
|
+
rescue JSON::ParserError
|
58
|
+
raise ParseError, "Response is not valid JSON"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Rach
|
2
|
+
module ResponseFormat
|
3
|
+
def self.included(base)
|
4
|
+
require 'json/schema_builder'
|
5
|
+
base.include JSON::SchemaBuilder
|
6
|
+
base.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def render(schema_name)
|
11
|
+
base_schema = new.public_send(schema_name)
|
12
|
+
schema = prepare_schema_for_api(base_schema)
|
13
|
+
|
14
|
+
{
|
15
|
+
type: "json_schema",
|
16
|
+
json_schema: {
|
17
|
+
name: schema_name.to_s,
|
18
|
+
schema:,
|
19
|
+
strict: true
|
20
|
+
},
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def prepare_schema_for_api(schema)
|
27
|
+
schema.additional_properties(false)
|
28
|
+
schema_hash = schema.as_json.deep_symbolize_keys
|
29
|
+
|
30
|
+
# Recursively set additional_properties: false and required for all objects
|
31
|
+
deep_set_object_properties(schema_hash)
|
32
|
+
|
33
|
+
schema_hash
|
34
|
+
end
|
35
|
+
|
36
|
+
def deep_set_object_properties(schema_hash)
|
37
|
+
return unless schema_hash.is_a?(Hash)
|
38
|
+
|
39
|
+
if schema_hash[:type] == "object"
|
40
|
+
schema_hash[:additionalProperties] = false
|
41
|
+
# Set required to include all properties if properties exist
|
42
|
+
if schema_hash[:properties]
|
43
|
+
schema_hash[:required] = schema_hash[:properties].keys
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
schema_hash.each_value do |value|
|
48
|
+
if value.is_a?(Hash)
|
49
|
+
deep_set_object_properties(value)
|
50
|
+
elsif value.is_a?(Array)
|
51
|
+
value.each { |item| deep_set_object_properties(item) }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Rach
|
2
|
+
class UsageTracker
|
3
|
+
def initialize
|
4
|
+
@total_prompt_tokens = 0
|
5
|
+
@total_completion_tokens = 0
|
6
|
+
@total_tokens = 0
|
7
|
+
@total_cached_tokens = 0
|
8
|
+
@total_reasoning_tokens = 0
|
9
|
+
@request_count = 0
|
10
|
+
end
|
11
|
+
|
12
|
+
def track(response)
|
13
|
+
return unless response.usage
|
14
|
+
|
15
|
+
@total_prompt_tokens += response.prompt_tokens
|
16
|
+
@total_completion_tokens += response.completion_tokens
|
17
|
+
@total_tokens += response.total_tokens
|
18
|
+
@total_cached_tokens += response.usage.dig("prompt_tokens_details", "cached_tokens") || 0
|
19
|
+
@total_reasoning_tokens += response.usage.dig("completion_tokens_details", "reasoning_tokens") || 0
|
20
|
+
@request_count += 1
|
21
|
+
end
|
22
|
+
|
23
|
+
def stats
|
24
|
+
{
|
25
|
+
prompt_tokens: @total_prompt_tokens,
|
26
|
+
completion_tokens: @total_completion_tokens,
|
27
|
+
total_tokens: @total_tokens,
|
28
|
+
cached_tokens: @total_cached_tokens,
|
29
|
+
reasoning_tokens: @total_reasoning_tokens,
|
30
|
+
request_count: @request_count,
|
31
|
+
average_tokens_per_request: @request_count > 0 ? (@total_tokens.to_f / @request_count).round(2) : 0
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/rach/version.rb
ADDED
data/lib/rach.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'openai'
|
2
|
+
|
3
|
+
require_relative "rach/version"
|
4
|
+
require_relative "rach/client"
|
5
|
+
require_relative "rach/errors"
|
6
|
+
require_relative "rach/response"
|
7
|
+
require_relative "rach/message"
|
8
|
+
require_relative "rach/message_template"
|
9
|
+
require_relative "rach/response_format"
|
10
|
+
require_relative "rach/conversation"
|
11
|
+
require_relative "rach/usage_tracker"
|
12
|
+
require_relative "rach/function"
|
13
|
+
|
14
|
+
module Rach
|
15
|
+
# Your code goes here...
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rach
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Roger Garcia
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-10-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.0'
|
34
|
+
- - "<"
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '3'
|
37
|
+
type: :development
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '1.0'
|
44
|
+
- - "<"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '3'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '13.0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '13.0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: ruby-openai
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '7.3'
|
68
|
+
type: :runtime
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '7.3'
|
75
|
+
description: Rach is a lightweight framework for orchestrating AI agents
|
76
|
+
email:
|
77
|
+
- rach@rogergarcia.me
|
78
|
+
executables: []
|
79
|
+
extensions: []
|
80
|
+
extra_rdoc_files: []
|
81
|
+
files:
|
82
|
+
- README.md
|
83
|
+
- lib/rach.rb
|
84
|
+
- lib/rach/client.rb
|
85
|
+
- lib/rach/conversation.rb
|
86
|
+
- lib/rach/errors.rb
|
87
|
+
- lib/rach/function.rb
|
88
|
+
- lib/rach/message.rb
|
89
|
+
- lib/rach/message_template.rb
|
90
|
+
- lib/rach/response.rb
|
91
|
+
- lib/rach/response_format.rb
|
92
|
+
- lib/rach/usage_tracker.rb
|
93
|
+
- lib/rach/version.rb
|
94
|
+
homepage: https://github.com/roginn/rach
|
95
|
+
licenses:
|
96
|
+
- MIT
|
97
|
+
metadata: {}
|
98
|
+
post_install_message:
|
99
|
+
rdoc_options: []
|
100
|
+
require_paths:
|
101
|
+
- lib
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
requirements: []
|
113
|
+
rubygems_version: 3.5.3
|
114
|
+
signing_key:
|
115
|
+
specification_version: 4
|
116
|
+
summary: Orchestrate AI agents like a virtuoso
|
117
|
+
test_files: []
|