ox-ai-workers 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.
@@ -0,0 +1,83 @@
1
+ class OxAiWorkers::ModuleRequest
2
+ attr_accessor :result, :client, :messages, :model, :max_tokens, :custom_id, :temperature, :tools, :errors
3
+ attr_accessor :tool_calls_raw, :tool_calls
4
+ DEFAULT_MODEL = "gpt-4o-mini"
5
+ DEFAULT_MAX_TOKEN = 4096
6
+ DEFAULT_TEMPERATURE = 0.7
7
+
8
+ def initializeRequests(model: DEFAULT_MODEL, max_tokens: DEFAULT_MAX_TOKEN, temperature: DEFAULT_TEMPERATURE)
9
+ puts "call: ModuleRequest::#{__method__}"
10
+ @max_tokens = max_tokens
11
+ @custom_id = SecureRandom.uuid
12
+ @model = model
13
+ @temperature = temperature
14
+ @client = nil
15
+
16
+ OxAiWorkers.configuration.access_token ||= ENV["OPENAI"]
17
+ if OxAiWorkers.configuration.access_token.nil?
18
+ error_text = "OpenAi access token missing!"
19
+ raise OxAiWorkers::ConfigurationError, error_text
20
+ end
21
+
22
+ cleanup()
23
+ end
24
+
25
+ def cleanup
26
+ puts "call: ModuleRequest::#{__method__}"
27
+ @client ||= OpenAI::Client.new(
28
+ access_token: OxAiWorkers.configuration.access_token,
29
+ log_errors: true # Highly recommended in development, so you can see what errors OpenAI is returning. Not recommended in production because it could leak private data to your logs.
30
+ )
31
+ @result = nil
32
+ @errors = nil
33
+ @messages = []
34
+ end
35
+
36
+ def append role: nil, content: nil, messages: nil
37
+ @messages << {role: role, content: content} if role.present? and content.present?
38
+ @messages += messages if messages.present?
39
+ end
40
+
41
+ def params
42
+ parameters = {
43
+ model: @model,
44
+ messages: @messages,
45
+ temperature: @temperature,
46
+ max_tokens: @max_tokens
47
+ }
48
+ if @tools.present?
49
+ parameters[:tools] = @tools
50
+ parameters[:tool_choice] = "required"
51
+ end
52
+ parameters
53
+ end
54
+
55
+ def not_found_is_ok &block
56
+ begin
57
+ yield
58
+ rescue Faraday::ResourceNotFound => e
59
+ nil
60
+ end
61
+ end
62
+
63
+ def parseChoices(response)
64
+ puts response.inspect
65
+ @tool_calls = []
66
+ @result = response.dig("choices", 0, "message", "content")
67
+ @tool_calls_raw = response.dig("choices", 0, "message", "tool_calls")
68
+
69
+ @tool_calls_raw.each do |tool|
70
+ function = tool["function"]
71
+ args = JSON.parse(YAML.load(function["arguments"]).to_json, { symbolize_names: true } )
72
+ @tool_calls << {
73
+ class: function["name"].split("__").first,
74
+ name: function["name"].split("__").last,
75
+ args: args
76
+ }
77
+ end
78
+
79
+ @tool_calls
80
+
81
+ # Assistant.send(function_name, **args)
82
+ end
83
+ end
@@ -0,0 +1,29 @@
1
+ module OxAiWorkers
2
+ module PresentCompat
3
+ def present?
4
+ !self.nil? and !self.empty?
5
+ end
6
+ end
7
+
8
+ module CamelizeCompat
9
+ def camelize(first_letter = :upper)
10
+ string = self.dup
11
+ if first_letter == :upper
12
+ string = string.sub(/^[a-z\d]*/) { |match| match.capitalize }
13
+ else
14
+ string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { |match| match.downcase }
15
+ end
16
+ string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub("/", "::")
17
+ end
18
+
19
+ def underscore
20
+ word = self.dup
21
+ word.gsub!(/::/, '/')
22
+ word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
23
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
24
+ word.tr!("-", "_")
25
+ word.downcase!
26
+ word
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ class OxAiWorkers::Request < OxAiWorkers::ModuleRequest
2
+ alias_method :initialize, :initializeRequests
3
+
4
+ def finish
5
+ @custom_id = SecureRandom.uuid
6
+ cleanup()
7
+ end
8
+
9
+ def request!
10
+ begin
11
+ response = @client.chat(parameters: params)
12
+ parseChoices(response)
13
+ #@result = response.dig("choices", 0, "message", "content")
14
+ #puts response.inspect
15
+ rescue OpenAI::Error => e
16
+ puts e.inspect
17
+ end
18
+ end
19
+
20
+ def completed?
21
+ @result.present? or @errors.present? or @external_call.present?
22
+ end
23
+ end
@@ -0,0 +1,61 @@
1
+ require 'state_machine/core'
2
+
3
+ class OxAiWorkers::StateBatch < OxAiWorkers::ModuleRequest
4
+ include OxAiWorkers::StateHelper
5
+ extend StateMachine::MacroMethods
6
+
7
+ alias_method :state_initialize, :initialize
8
+ attr_accessor :file_id, :batch_id
9
+
10
+ state_machine :batch_state, initial: ->(t){t.batch_id.present? ? :requested : :idle}, namespace: :batch do
11
+ before_transition from: any, do: :log_me
12
+
13
+ after_transition on: :end, do: :cleanup
14
+ before_transition on: :process, do: :postBatch
15
+ after_transition on: :cancel, do: [:cancelBatch, :complete_batch!]
16
+ after_transition on: :complete, do: [:cleanStorage]
17
+ after_transition on: :prepare, do: :uploadToStorage
18
+
19
+ event :end do
20
+ transition [:finished, :canceled] => :idle
21
+ end
22
+
23
+ event :prepare do
24
+ transition :idle => :prepared
25
+ end
26
+
27
+ event :process do
28
+ transition :prepared => :requested
29
+ end
30
+
31
+ event :complete do
32
+ transition [:requested, :failed] => :finished
33
+ end
34
+
35
+ event :cancel do
36
+ transition [:requested, :failed, :prepared] => :canceled
37
+ end
38
+
39
+ event :fail do
40
+ transition [:requested, :prepared] => :failed
41
+ end
42
+
43
+ state :requested
44
+ state :idle
45
+ state :prepared
46
+ state :canceled
47
+ state :finished
48
+ state :failed
49
+ end
50
+
51
+ def cleanup
52
+ @file_id = nil
53
+ @batch_id = nil
54
+ super()
55
+ end
56
+
57
+ def initialize
58
+ puts "call: StateBatch::#{__method__}"
59
+ super()
60
+ end
61
+ end
@@ -0,0 +1,5 @@
1
+ module OxAiWorkers::StateHelper
2
+ def log_me(transition)
3
+ # puts "`#{transition.event}` was called to transition from :#{transition.from} to :#{transition.to}"
4
+ end
5
+ end
@@ -0,0 +1,47 @@
1
+ require 'state_machine/core'
2
+
3
+ class OxAiWorkers::StateTools
4
+ include OxAiWorkers::StateHelper
5
+ extend StateMachine::MacroMethods
6
+
7
+ state_machine :state, initial: :idle do
8
+ before_transition from: any, do: :log_me
9
+
10
+ after_transition on: :iterate, do: :nextIteration
11
+ after_transition on: :request, do: :externalRequest
12
+ after_transition on: :prepare, do: :init
13
+ after_transition on: :analyze, do: :processResult
14
+ after_transition on: :complete, do: :completeIteration
15
+
16
+ event :prepare do
17
+ transition [:idle, :finished] => :prepared
18
+ end
19
+
20
+ event :request do
21
+ transition :prepared => :requested
22
+ end
23
+
24
+ event :analyze do
25
+ transition [:requested] => :analyzed
26
+ end
27
+
28
+ event :complete do
29
+ transition [:analyzed] => :finished
30
+ end
31
+
32
+ event :iterate do
33
+ transition :analyzed => :prepared
34
+ end
35
+
36
+ event :end do
37
+ transition :finished => :idle
38
+ end
39
+
40
+ state :idle
41
+ state :prepared
42
+ state :requested
43
+ state :analyzed
44
+ state :finished
45
+ end
46
+
47
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OxAiWorkers::Tool
4
+ #
5
+ # A calculator tool that falls back to the Google calculator widget
6
+ #
7
+ # Gem requirements:
8
+ # gem "eqn", "~> 1.6.5"
9
+ # gem "google_search_results", "~> 2.0.0"
10
+ #
11
+ # Usage:
12
+ # calculator = OxAiWorkers::Tool::Calculator.new
13
+ #
14
+ class Eval
15
+ extend OxAiWorkers::ToolDefinition
16
+ include OxAiWorkers::DependencyHelper
17
+
18
+ define_function :ruby, description: I18n.t("oxaiworkers.tool.eval.ruby.description") do
19
+ property :input, type: "string", description: I18n.t("oxaiworkers.tool.eval.ruby.input"), required: true
20
+ end
21
+
22
+ define_function :sh, description: I18n.t("oxaiworkers.tool.eval.sh.description") do
23
+ property :input, type: "string", description: I18n.t("oxaiworkers.tool.eval.sh.input"), required: true
24
+ end
25
+
26
+ def ruby(input:)
27
+ puts Rainbow("Executing ruby: \"#{input}\"").red
28
+ eval(input)
29
+ end
30
+
31
+ def sh(input:)
32
+ puts Rainbow("Executing sh: \"#{input}\"").red
33
+ stdout_and_stderr_s, _ = Open3.capture2e(input)
34
+ return stdout_and_stderr_s
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ #
6
+ # Extends a class to be used as a tool in the assistant.
7
+ # A tool is a collection of functions (methods) used to perform specific tasks.
8
+ #
9
+ # == Usage
10
+ #
11
+ # 1. Extend your class with {OxAiWorkers::ToolDefinition}
12
+ # 2. Use {#define_function} to define each function of the tool
13
+ #
14
+ # == Key Concepts
15
+ #
16
+ # - {#define_function}: Defines a new function (method) for the tool
17
+ # - {ParameterBuilder#property}: Defines properties for the function parameters
18
+ # - {ParameterBuilder#item}: Alias for {ParameterBuilder#property}, used for array items
19
+ #
20
+ # These methods support various data types and nested structures, allowing for flexible and expressive tool definitions.
21
+ #
22
+ # @example Defining a tool with various property types and configurations
23
+ # define_function :sample_function, description: "Demonstrates various property types and configurations" do
24
+ # property :string_prop, type: "string", description: "A simple string property"
25
+ # property :number_prop, type: "number", description: "A number property"
26
+ # property :integer_prop, type: "integer", description: "An integer property"
27
+ # property :boolean_prop, type: "boolean", description: "A boolean property"
28
+ # property :enum_prop, type: "string", description: "An enum property", enum: ["option1", "option2", "option3"]
29
+ # property :required_prop, type: "string", description: "A required property", required: true
30
+ # property :array_prop, type: "array", description: "An array property" do
31
+ # item type: "string", description: "Array item"
32
+ # end
33
+ # property :object_prop, type: "object", description: "An object property" do
34
+ # property :nested_string, type: "string", description: "Nested string property"
35
+ # property :nested_number, type: "number", description: "Nested number property"
36
+ # end
37
+ # end
38
+ #
39
+ module OxAiWorkers::ToolDefinition
40
+ # Defines a function for the tool
41
+ #
42
+ # @param method_name [Symbol] Name of the method to define
43
+ # @param description [String] Description of the function
44
+ # @yield Block that defines the parameters for the function
45
+ def define_function(method_name, description:, &)
46
+ function_schemas.add_function(method_name:, description:, &)
47
+ end
48
+
49
+ # Returns the FunctionSchemas instance for this tool
50
+ #
51
+ # @return [FunctionSchemas] The FunctionSchemas instance
52
+ def function_schemas
53
+ @function_schemas ||= FunctionSchemas.new(tool_name)
54
+ end
55
+
56
+ # Returns the snake_case version of the class name as the tool's name
57
+ #
58
+ # @return [String] The snake_case version of the class name
59
+ def tool_name
60
+ @tool_name ||= name
61
+ .gsub("::", "_")
62
+ .gsub(/(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-z\d])(?=[A-Z])/, "_")
63
+ .downcase
64
+ end
65
+
66
+ # Manages schemas for functions
67
+ class FunctionSchemas
68
+ def initialize(tool_name)
69
+ @schemas = {}
70
+ @tool_name = tool_name
71
+ end
72
+
73
+ # Adds a function to the schemas
74
+ #
75
+ # @param method_name [Symbol] Name of the method to add
76
+ # @param description [String] Description of the function
77
+ # @yield Block that defines the parameters for the function
78
+ # @raise [ArgumentError] If a block is defined and no parameters are specified for the function
79
+ def add_function(method_name:, description:, &)
80
+ name = "#{@tool_name}__#{method_name}"
81
+
82
+ if block_given?
83
+ parameters = ParameterBuilder.new(parent_type: "object").build(&)
84
+
85
+ if parameters[:properties].empty?
86
+ raise ArgumentError, "Function parameters must have at least one property defined within it, if a block is provided"
87
+ end
88
+ end
89
+
90
+ @schemas[method_name] = {
91
+ type: "function",
92
+ function: {name:, description:, parameters:}.compact
93
+ }
94
+ end
95
+
96
+ # Converts schemas to OpenAI-compatible format
97
+ #
98
+ # @return [String] JSON string of schemas in OpenAI format
99
+ def to_openai_format
100
+ @schemas.values#.map { |schema| schema[:function] }
101
+ end
102
+
103
+ # Converts schemas to Anthropic-compatible format
104
+ #
105
+ # @return [String] JSON string of schemas in Anthropic format
106
+ def to_anthropic_format
107
+ @schemas.values.map do |schema|
108
+ schema[:function].transform_keys("parameters" => "input_schema")
109
+ end
110
+ end
111
+
112
+ # Converts schemas to Google Gemini-compatible format
113
+ #
114
+ # @return [String] JSON string of schemas in Google Gemini format
115
+ def to_google_gemini_format
116
+ @schemas.values.map { |schema| schema[:function] }
117
+ end
118
+ end
119
+
120
+ # Builds parameter schemas for functions
121
+ class ParameterBuilder
122
+ VALID_TYPES = %w[object array string number integer boolean].freeze
123
+
124
+ def initialize(parent_type:)
125
+ @schema = (parent_type == "object") ? {type: "object", properties: {}, required: []} : {}
126
+ @parent_type = parent_type
127
+ end
128
+
129
+ # Builds the parameter schema
130
+ #
131
+ # @yield Block that defines the properties of the schema
132
+ # @return [Hash] The built schema
133
+ def build(&)
134
+ instance_eval(&)
135
+ @schema
136
+ end
137
+
138
+ # Defines a property in the schema
139
+ #
140
+ # @param name [Symbol] Name of the property (required only for a parent of type object)
141
+ # @param type [String] Type of the property
142
+ # @param description [String] Description of the property
143
+ # @param enum [Array] Array of allowed values
144
+ # @param required [Boolean] Whether the property is required
145
+ # @yield [Block] Block for nested properties (only for object and array types)
146
+ # @raise [ArgumentError] If any parameter is invalid
147
+ def property(name = nil, type:, description: nil, enum: nil, required: false, &)
148
+ validate_parameters(name:, type:, enum:, required:)
149
+
150
+ prop = {type:, description:, enum:}.compact
151
+
152
+ if block_given?
153
+ nested_schema = ParameterBuilder.new(parent_type: type).build(&)
154
+
155
+ case type
156
+ when "object"
157
+ if nested_schema[:properties].empty?
158
+ raise ArgumentError, "Object properties must have at least one property defined within it"
159
+ end
160
+ prop = nested_schema
161
+ when "array"
162
+ if nested_schema.empty?
163
+ raise ArgumentError, "Array properties must have at least one item defined within it"
164
+ end
165
+ prop[:items] = nested_schema
166
+ end
167
+ end
168
+
169
+ if @parent_type == "object"
170
+ @schema[:properties][name] = prop
171
+ @schema[:required] << name.to_s if required
172
+ else
173
+ @schema = prop
174
+ end
175
+ end
176
+
177
+ # Alias for property method, used for defining array items
178
+ alias_method :item, :property
179
+
180
+ private
181
+
182
+ # Validates the parameters for a property
183
+ #
184
+ # @param name [Symbol] Name of the property
185
+ # @param type [String] Type of the property
186
+ # @param enum [Array] Array of allowed values
187
+ # @param required [Boolean] Whether the property is required
188
+ # @raise [ArgumentError] If any parameter is invalid
189
+ def validate_parameters(name:, type:, enum:, required:)
190
+ if @parent_type == "object"
191
+ if name.nil?
192
+ raise ArgumentError, "Name must be provided for properties of an object"
193
+ end
194
+ unless name.is_a?(Symbol)
195
+ raise ArgumentError, "Invalid name '#{name}'. Name must be a symbol"
196
+ end
197
+ end
198
+
199
+ unless VALID_TYPES.include?(type)
200
+ raise ArgumentError, "Invalid type '#{type}'. Valid types are: #{VALID_TYPES.join(", ")}"
201
+ end
202
+
203
+ unless enum.nil? || enum.is_a?(Array)
204
+ raise ArgumentError, "Invalid enum '#{enum}'. Enum must be nil or an array"
205
+ end
206
+
207
+ unless [true, false].include?(required)
208
+ raise ArgumentError, "Invalid required '#{required}'. Required must be a boolean"
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OxAiWorkers
4
+ VERSION = "0.1.0"
5
+ end
6
+
@@ -0,0 +1,2 @@
1
+ require_relative "../oxaiworkers"
2
+ require_relative "../oxaiworkers/compatibility"
@@ -0,0 +1,35 @@
1
+ en:
2
+ oxaiworkers:
3
+ iterator:
4
+ inner_monologue:
5
+ description: "Use inner monologue to plan the response and articulate main points"
6
+ speech: "Text"
7
+ outer_voice:
8
+ description: "Provide the user with necessary information without expecting a response"
9
+ text: "Text"
10
+ action_request:
11
+ description: "Ask a clarifying question or request an action with a response from the user"
12
+ action: "Text"
13
+ pack_history:
14
+ description: "Save facts, nuances, and actions before clearing messages"
15
+ text: "Listing important facts and nuances"
16
+ monologue:
17
+ steps:
18
+ - "Step 1: Develop your own solution to the problem. Take initiative and make assumptions."
19
+ - "Step 1.1: Wrap all your work for this step in the innerMonologue function."
20
+ - "Step 2: Relate your solution to the task, improve it, and call the necessary functions step by step."
21
+ - "Step 2.1: Interact with the user using the outerVoice and actionRequest functions during the process."
22
+ - "Step 3: When the solution is ready, report it using the outerVoice function."
23
+ - "Step 3.1: Save facts, nuances, and actions using the packHistory function."
24
+ - "Step 4: Conclude the work with the actionRequest function, without repeating the response if it was already given with outerVoice."
25
+ tool:
26
+ eval:
27
+ ruby:
28
+ description: "Execute Ruby code and return the result of the last expression"
29
+ input: "Ruby source code"
30
+ sh:
31
+ description: "Execute a sh command and get the result (stdout + stderr)"
32
+ input: "Source command"
33
+ assistant:
34
+ sysop:
35
+ role: "You are a software agent inside my computer"
@@ -0,0 +1,34 @@
1
+ ru:
2
+ oxaiworkers:
3
+ iterator:
4
+ inner_monologue:
5
+ description: Используй внутренний монолог для планирования ответа и проговаривания основных тезисов
6
+ speach: Текст
7
+ outer_voice:
8
+ description: Сообщить пользователю необходимую информацию без ожидания ответа
9
+ text: Текст
10
+ action_request:
11
+ description: Задать уточняющий вопрос или попросить о действии с получением ответа от пользователя
12
+ action: Текст
13
+ pack_history:
14
+ description: Сохранение фактов, нюансов и действий (в том числе что ответ был дан) перед очисткой сообщений
15
+ text: Перечисление важных фактов и нюансов
16
+ monologue:
17
+ - Шаг 1. Разработай собственное решение проблемы. Проявляй инициативу и делай предположения.
18
+ - Шаг 1.1. Заключи все свои наработки для этого шага в функцию innerMonologue.
19
+ - Шаг 2. Соотнеси свое решение с задачей, улучшай его и вызывай необходимые функции шаг за шагом.
20
+ - Шаг 2.1. Во время работы используй функции outerVoice и actionRequest.
21
+ - Шаг 3. Когда решение готово, сообщи о нем с помощью функции outerVoice.
22
+ - Шаг 3.1. Сохрани факты, нюансы и действия с помощью функции packHistory.
23
+ - Шаг 4. Заверши работу с помощью функции actionRequest, не повторяя ответ, если он уже был дан.
24
+ tool:
25
+ eval:
26
+ ruby:
27
+ description: Выполнить код на ruby и вернуть результат последней строки
28
+ input: Исходный код на Ruby
29
+ sh:
30
+ description: Выполнить sh-команду и получить результат (stdout + stderr)
31
+ input: Исходная команда
32
+ assistant:
33
+ sysop:
34
+ role: Ты программный агент внутри моего компьютера