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 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.
@@ -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
@@ -0,0 +1,4 @@
1
+ module Rach
2
+ class Error < StandardError; end
3
+ class ParseError < Error; end
4
+ end
@@ -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
@@ -0,0 +1,17 @@
1
+ module Rach
2
+ class Message
3
+ attr_reader :role, :content
4
+
5
+ def initialize(content:, role: "user")
6
+ @content = content
7
+ @role = role
8
+ end
9
+
10
+ def to_h
11
+ {
12
+ role: role,
13
+ content: content
14
+ }
15
+ end
16
+ end
17
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Rach
2
+ VERSION = "0.1.0"
3
+ end
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: []