rach 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|