omni_agent 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: 3a96ab390eb98871a6b9133599479381dda9ccfeee7b859c5a6641a5fe027c17
4
+ data.tar.gz: b4928a418a02e6178e338a00694f03d75b24f854ee85d49eb52f6125b683f0c0
5
+ SHA512:
6
+ metadata.gz: cf525ecac3cf4a76815a007560d49c6049440d377aad441cca96aaac7df962e5dffd58672def02511a34d7a851c0ff2611b4ce0912b6b56abaaac032e811eabf
7
+ data.tar.gz: c1d64f9b290de984171aae25a1fdb2912a9ed8b395dec7ad1a9fa66af4a5d7743ac2aff4f7caab6108293cb050036aa8791e55491043846177cce43ea4e29a34
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0] - 2026-06-09
8
+
9
+ ### Added
10
+ - Initial release of OmniAgent as a Rails engine for building application-native AI agents.
11
+ - Agent runtime with provider abstraction, tool-calling loop, callbacks, and prompt rendering.
12
+ - OpenAI provider support via the `openai` gem.
13
+ - Tool DSL with JSON schema generation.
14
+ - Rails generators and tasks for agent scaffolding.
15
+ - RSpec coverage for agent runtime, providers, tools, and integration flows.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright ACR1209
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # OmniAgent
2
+
3
+ OmniAgent is a Rails engine gem for building application-native AI agents with tools.
4
+ It provides a small DSL to define agents, model/provider settings, prompt templates,
5
+ tool schemas, and generation lifecycle callbacks.
6
+
7
+ ## What It Includes
8
+
9
+ - `OmniAgent::Agent` runtime with provider abstraction and tool-calling loop
10
+ - `OmniAgent::Tool` DSL with JSON-schema-style input definitions
11
+ - Prompt composition from ERB files in `app/agents/<agent_name>/`
12
+ - Agent callbacks (`before_generation`, `after_generation`)
13
+ - Agent and tool tags to support filtering strategies
14
+ - OpenAI provider integration out of the box
15
+ - Rake tasks and Rails generators for scaffolding
16
+
17
+ ## Installation
18
+
19
+ Add these lines to your application's Gemfile:
20
+
21
+ ```ruby
22
+ gem "omni_agent"
23
+ ```
24
+
25
+ Add the provider you're using to the Gemfile as well:
26
+ ```ruby
27
+ gem "openai"
28
+ ```
29
+
30
+ Then run:
31
+
32
+ ```bash
33
+ bundle install
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ 1. Install base directories:
39
+
40
+ ```bash
41
+ bundle exec rails generate omni_agent:install
42
+ ```
43
+
44
+ 2. Generate an agent scaffold:
45
+
46
+ ```bash
47
+ bundle exec rails generate omni_agent:agent ResearchAgent --model gpt-4.1-mini --with-tools WeatherLookup Summarize
48
+ ```
49
+
50
+ 3. Add your API key in `.env`:
51
+
52
+ ```dotenv
53
+ OPENAI_ACCESS_TOKEN=your_api_key_here
54
+ ```
55
+
56
+ 4. Implement your agent prompt and optional tools under:
57
+
58
+ ```text
59
+ app/agents/research_agent/
60
+ research_agent.rb
61
+ prompt.md.erb
62
+ tools/
63
+ ```
64
+
65
+ ## Agent Example
66
+
67
+ ```ruby
68
+ class ResearchAgent < OmniAgent::Agent
69
+ use_model "gpt-4o-mini"
70
+
71
+ before_generation :set_current_user
72
+
73
+ def set_current_user
74
+ @user = "Test User"
75
+ end
76
+ end
77
+ ```
78
+
79
+ ## Tool Example
80
+
81
+ ```ruby
82
+ module ResearchAgent::Tools
83
+ class GetWeather < OmniAgent::Tool
84
+ description "Get current weather for a city"
85
+ tags :weather
86
+ metadata category: :utility
87
+
88
+ input do
89
+ string :city, description: "City name"
90
+ end
91
+
92
+ def execute(city:)
93
+ "Sunny in #{city}"
94
+ end
95
+ end
96
+ end
97
+ ```
98
+
99
+ ## Configuration
100
+
101
+ Global defaults can be configured through `OmniAgent.configure`:
102
+
103
+ ```ruby
104
+ OmniAgent.configure do |config|
105
+ config.default_provider = :openai
106
+ config.default_model = "gpt-4o-mini"
107
+ end
108
+ ```
109
+
110
+ ## Running Tests
111
+
112
+ ```bash
113
+ bundle exec rspec
114
+ ```
115
+
116
+ ## Contributing
117
+
118
+ Issues and pull requests are welcome.
119
+
120
+ ## License
121
+
122
+ The gem is available as open source under the terms of the
123
+ [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/setup"
2
+ require "rspec/core/rake_task"
3
+
4
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
5
+ load "rails/tasks/engine.rake"
6
+
7
+ require "bundler/gem_tasks"
8
+
9
+ RSpec::Core::RakeTask.new(:spec)
10
+
11
+ task default: :spec
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,4 @@
1
+ module OmniAgent
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module OmniAgent
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module OmniAgent
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module OmniAgent
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module OmniAgent
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Omni agent</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= yield :head %>
9
+
10
+ <%= stylesheet_link_tag "omni_agent/application", media: "all" %>
11
+ </head>
12
+ <body>
13
+
14
+ <%= yield %>
15
+
16
+ </body>
17
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ OmniAgent::Engine.routes.draw do
2
+ end
@@ -0,0 +1,103 @@
1
+ require "rails/generators/named_base"
2
+ require "fileutils"
3
+
4
+ module OmniAgent
5
+ module Generators
6
+ class AgentGenerator < Rails::Generators::NamedBase
7
+ class_option :with_tools,
8
+ type: :array,
9
+ default: nil,
10
+ banner: "[Tool1 Tool2 ...]",
11
+ desc: "Create tools folder and scaffold tools. Optionally pass tool names"
12
+ class_option :model,
13
+ type: :string,
14
+ default: nil,
15
+ desc: "Override the model used in use_model for the generated agent"
16
+
17
+ def create_agent_structure
18
+ FileUtils.mkdir_p(agent_directory)
19
+
20
+ create_file(agent_file_path, <<~RUBY)
21
+ class #{class_name} < OmniAgent::Agent
22
+ use_model #{generated_model_expression}
23
+ end
24
+ RUBY
25
+
26
+ create_file(prompt_file_path, <<~ERB)
27
+ You are #{class_name}, a helpful assistant with access to local tools.
28
+ ERB
29
+ end
30
+
31
+ def create_tools_if_requested
32
+ return unless with_tools_requested?
33
+
34
+ FileUtils.mkdir_p(tools_directory)
35
+
36
+ scaffold_names = requested_tool_names
37
+ scaffold_names.each do |tool_name|
38
+ create_file(tool_file_path(tool_name), tool_template(tool_name))
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def agent_directory
45
+ File.join(destination_root, "app", "agents", file_name)
46
+ end
47
+
48
+ def agent_file_path
49
+ File.join(agent_directory, "#{file_name}.rb")
50
+ end
51
+
52
+ def prompt_file_path
53
+ File.join(agent_directory, "prompt.md.erb")
54
+ end
55
+
56
+ def tools_directory
57
+ File.join(agent_directory, "tools")
58
+ end
59
+
60
+ def tool_file_path(tool_name)
61
+ File.join(tools_directory, "#{tool_name.to_s.underscore}.rb")
62
+ end
63
+
64
+ def tool_template(tool_name)
65
+ tool_class = tool_name.to_s.camelize
66
+
67
+ <<~RUBY
68
+ module #{class_name}::Tools
69
+ class #{tool_class} < OmniAgent::Tool
70
+ description "#{tool_class} generated by omni_agent:agent"
71
+ tags :example
72
+ metadata category: :demo
73
+
74
+ input do
75
+ string :input_text, description: "Text input"
76
+ end
77
+
78
+ def execute(input_text:)
79
+ "Echo: \#{input_text}"
80
+ end
81
+ end
82
+ end
83
+ RUBY
84
+ end
85
+
86
+ def with_tools_requested?
87
+ !options[:with_tools].nil?
88
+ end
89
+
90
+ def requested_tool_names
91
+ names = Array(options[:with_tools]).map(&:to_s).map(&:strip).reject(&:empty?)
92
+ names.empty? ? ["ExampleTool"] : names
93
+ end
94
+
95
+ def generated_model_expression
96
+ model = options[:model].to_s.strip
97
+ return "OmniAgent.configuration.default_model" if model.empty?
98
+
99
+ model.inspect
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,21 @@
1
+ require "rails/generators"
2
+ require "fileutils"
3
+
4
+ module OmniAgent
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ desc "Creates OmniAgent base directories in app/agents"
8
+
9
+ def create_agents_root
10
+ agents_root = File.join(destination_root, "app", "agents")
11
+ FileUtils.mkdir_p(agents_root)
12
+ say_status :create, agents_root
13
+ end
14
+
15
+ def show_next_steps
16
+ say "OmniAgent install complete."
17
+ say "Next: rails generate omni_agent:agent SupportAgent --with-tools Tool1 Tool2"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,328 @@
1
+ module OmniAgent
2
+ class Agent
3
+ attr_reader :provider
4
+
5
+ module ImplicitRunEntrypoints
6
+ def method_added(method_name)
7
+ super
8
+
9
+ return if @_omni_agent_wrapping_method
10
+ return if method_name.to_s.start_with?("__omni_agent_original_")
11
+ return unless instance_methods(false).include?(method_name)
12
+ return if OmniAgent::Agent.instance_methods(false).include?(method_name)
13
+
14
+ original_method = instance_method(method_name)
15
+ return unless original_method.arity == 0
16
+
17
+ alias_name = "__omni_agent_original_#{method_name}".to_sym
18
+ return if instance_methods(false).include?(alias_name)
19
+
20
+ @_omni_agent_wrapping_method = true
21
+ alias_method alias_name, method_name
22
+
23
+ define_method(method_name) do |input = nil, context: {}, **context_keywords|
24
+ if input.nil? && context == {} && context_keywords.empty?
25
+ run_alias_entrypoint_logic(alias_name)
26
+ else
27
+ merged_context = context.is_a?(Hash) ? context.dup : {}
28
+ merged_context.merge!(context_keywords)
29
+
30
+ run_input = run_alias_entrypoint_logic(alias_name, fallback_input: input)
31
+
32
+ run(run_input, context: merged_context, prompt_method: method_name)
33
+ end
34
+ end
35
+ ensure
36
+ @_omni_agent_wrapping_method = false
37
+ end
38
+ end
39
+
40
+ class << self
41
+ def provider(name, **options)
42
+ if configured_with_use_model?
43
+ raise OmniAgent::Error, "Cannot combine `provider` and `use_model` in the same agent. Use either `provider ..., model: ...` or `use_model ...`."
44
+ end
45
+
46
+ @provider_name = name
47
+ @provider_options = options
48
+ end
49
+
50
+ def options(**options)
51
+ @model_options = configured_model_options.merge(options)
52
+ end
53
+
54
+ def use_model(name)
55
+ if configured_provider_name || configured_provider_options.any?
56
+ raise OmniAgent::Error, "Cannot combine `provider` and `use_model` in the same agent. Use either `provider ..., model: ...` or `use_model ...`."
57
+ end
58
+
59
+ @configured_with_use_model = true
60
+ @provider_options = { model: name }
61
+ end
62
+
63
+ def before_generation(*callbacks)
64
+ @before_generation_callbacks = configured_before_generation_callbacks + normalize_callbacks(:before_generation, callbacks)
65
+ end
66
+
67
+ def after_generation(*callbacks)
68
+ @after_generation_callbacks = configured_after_generation_callbacks + normalize_callbacks(:after_generation, callbacks)
69
+ end
70
+
71
+ def tags(*tag_names)
72
+ return @configured_tags || [] if tag_names.empty?
73
+
74
+ @configured_tags = (tags + normalize_tags(tag_names)).uniq
75
+ end
76
+
77
+ def run_aliases(*method_names)
78
+ aliases = normalize_callbacks(:run_aliases, method_names)
79
+
80
+ aliases.each do |method_name|
81
+ define_method(method_name) do |input, context: {}|
82
+ run(input, context: context, prompt_method: method_name)
83
+ end
84
+ end
85
+ end
86
+
87
+ def with(context = nil, provider_override: nil, model_override: nil, options_override: {}, **context_keywords)
88
+ merged_context = {}
89
+ merged_context.merge!(context) if context.is_a?(Hash)
90
+ merged_context.merge!(context_keywords)
91
+
92
+ new(
93
+ provider_override: provider_override,
94
+ model_override: model_override,
95
+ options_override: options_override,
96
+ context_override: merged_context
97
+ )
98
+ end
99
+
100
+ def configured_provider_name; @provider_name; end
101
+ def configured_provider_options; @provider_options || {}; end
102
+ def configured_model_options; @model_options || {}; end
103
+ def configured_with_use_model?; @configured_with_use_model == true; end
104
+ def configured_before_generation_callbacks; @before_generation_callbacks || []; end
105
+ def configured_after_generation_callbacks; @after_generation_callbacks || []; end
106
+ def configured_tags; tags; end
107
+
108
+ def inherited(subclass)
109
+ super
110
+ subclass.extend(ImplicitRunEntrypoints)
111
+ end
112
+
113
+ private
114
+
115
+ def normalize_callbacks(callback_type, callbacks)
116
+ raise ArgumentError, "#{callback_type} requires at least one method name" if callbacks.empty?
117
+
118
+ callbacks.map do |callback|
119
+ unless callback.is_a?(String) || callback.is_a?(Symbol)
120
+ raise ArgumentError, "#{callback_type} callbacks must be method names"
121
+ end
122
+
123
+ callback.to_sym
124
+ end
125
+ end
126
+
127
+ def normalize_tags(tag_names)
128
+ raise ArgumentError, "tags requires at least one tag" if tag_names.empty?
129
+
130
+ tag_names.map do |tag_name|
131
+ unless tag_name.is_a?(String) || tag_name.is_a?(Symbol)
132
+ raise ArgumentError, "tags must be strings or symbols"
133
+ end
134
+
135
+ tag_name.to_sym
136
+ end
137
+ end
138
+ end
139
+
140
+ def initialize(provider_override: nil, model_override: nil, options_override: {}, context_override: {})
141
+ target_provider_name = provider_override || self.class.configured_provider_name || OmniAgent.configuration.default_provider
142
+ target_model = model_override || self.class.configured_provider_options[:model]
143
+ @chat_options = self.class.configured_model_options.merge(options_override)
144
+ @provider = resolve_provider(target_provider_name, target_model)
145
+ @default_context = context_override || {}
146
+ end
147
+
148
+ def run(input, context: {}, prompt_method: nil)
149
+ context = @default_context.merge(context || {})
150
+
151
+ messages = [
152
+ { role: "system", content: nil },
153
+ { role: "user", content: input }
154
+ ]
155
+
156
+ run_before_generation_callbacks(input: input, context: context, messages: messages)
157
+ messages[0][:content] = system_prompt(context: context, prompt_method: prompt_method)
158
+
159
+ filtered_tools = tool_filter(tools: available_tools, agent_tags: self.class.tags)
160
+
161
+ loop do
162
+ response = provider.chat(messages: messages, tools: filtered_tools, **@chat_options)
163
+
164
+ if response.content && !response.tool_calls?
165
+ messages << { role: "assistant", content: response.content }
166
+ run_after_generation_callbacks(input: input, context: context, messages: messages, response: response)
167
+ return response.content
168
+ end
169
+
170
+ messages << {
171
+ role: "assistant",
172
+ content: response.content,
173
+ tool_calls: response.raw_response.dig("choices", 0, "message", "tool_calls")
174
+ }
175
+
176
+ response.tool_calls.each do |tool_call|
177
+ tool_name = tool_call[:name]
178
+ tool_args = tool_call[:arguments]
179
+ tool_id = tool_call[:id]
180
+
181
+ tool_class = filtered_tools.find { |t| t.name.demodulize == tool_name }
182
+
183
+ if tool_class
184
+ begin
185
+ result = tool_class.invoke(tool_args)
186
+
187
+ messages << {
188
+ role: "tool",
189
+ tool_call_id: tool_id,
190
+ name: tool_name,
191
+ content: result.to_s
192
+ }
193
+ rescue => e
194
+ messages << {
195
+ role: "tool",
196
+ tool_call_id: tool_id,
197
+ name: tool_name,
198
+ content: "Error executing tool: #{e.message}"
199
+ }
200
+ end
201
+ else
202
+ messages << {
203
+ role: "tool",
204
+ tool_call_id: tool_id,
205
+ name: tool_name,
206
+ content: "Error: Tool #{tool_name} is not registered to this agent."
207
+ }
208
+ end
209
+ end
210
+ end
211
+ end
212
+
213
+ def available_tools
214
+ tool_namespace = "#{self.class.name}::Tools".safe_constantize
215
+ return [] unless tool_namespace
216
+
217
+ tool_namespace.constants.filter_map do |const_name|
218
+ const = tool_namespace.const_get(const_name)
219
+ const if const.is_a?(Class) && const < OmniAgent::Tool
220
+ end
221
+ end
222
+
223
+ private
224
+
225
+ def tool_filter(tools:, agent_tags:)
226
+ tools
227
+ end
228
+
229
+ def run_alias_entrypoint_logic(alias_name, fallback_input: nil)
230
+ @message = nil
231
+ result = public_send(alias_name)
232
+ computed_message = result.nil? ? @message : result
233
+
234
+ return computed_message if fallback_input.nil?
235
+
236
+ fallback_input
237
+ end
238
+
239
+ def resolve_provider(name, model)
240
+ OmniAgent::Providers.registry[name.to_sym].new(model: model)
241
+ end
242
+
243
+ def run_before_generation_callbacks(input:, context:, messages:)
244
+ payload = { input: input, context: context, messages: messages }
245
+
246
+ self.class.configured_before_generation_callbacks.each do |callback_name|
247
+ invoke_generation_callback(callback_name, payload)
248
+ end
249
+ end
250
+
251
+ def run_after_generation_callbacks(input:, context:, messages:, response:)
252
+ payload = { input: input, context: context, messages: messages, response: response }
253
+
254
+ self.class.configured_after_generation_callbacks.each do |callback_name|
255
+ invoke_generation_callback(callback_name, payload)
256
+ end
257
+ end
258
+
259
+ def invoke_generation_callback(callback_name, payload)
260
+ original_callback_name = "__omni_agent_original_#{callback_name}".to_sym
261
+ callback_target = if respond_to?(original_callback_name, true)
262
+ original_callback_name
263
+ else
264
+ callback_name
265
+ end
266
+
267
+ callback_method = self.class.instance_method(callback_target)
268
+
269
+ if callback_method.arity == 0
270
+ __send__(callback_target)
271
+ else
272
+ __send__(callback_target, payload)
273
+ end
274
+ end
275
+
276
+ def system_prompt(context:, prompt_method: nil)
277
+ return "You are a helpful assistant with access to local tools." unless defined?(Rails)
278
+
279
+ class_name = self.class.name
280
+ return "You are a helpful assistant with access to local tools." if class_name.nil?
281
+
282
+ agent_dir = inflector_underscore(class_name)
283
+ base_file_path = Rails.root.join("app", "agents", agent_dir, "prompt.md.erb")
284
+ method_file_path = if prompt_method
285
+ prompt_method_name = inflector_underscore(prompt_method.to_s)
286
+ Rails.root.join("app", "agents", agent_dir, "#{prompt_method_name}.md.erb")
287
+ end
288
+
289
+ isolated_scope = Object.new
290
+
291
+ context.each do |key, value|
292
+ isolated_scope.instance_variable_set("@#{key}", value)
293
+ end
294
+
295
+ internal_vars = [:@provider, :@chat_options] # Blacklists internal instance variables
296
+
297
+ (instance_variables - internal_vars).each do |ivar|
298
+ isolated_scope.instance_variable_set(ivar, instance_variable_get(ivar))
299
+ end
300
+
301
+ base_prompt = render_prompt_template(base_file_path, isolated_scope)
302
+ method_prompt = render_prompt_template(method_file_path, isolated_scope)
303
+
304
+ prompts = [base_prompt, method_prompt].compact.reject(&:empty?)
305
+ return prompts.join("\n\n") if prompts.any?
306
+
307
+ "You are a helpful assistant with access to local tools."
308
+ end
309
+
310
+ def render_prompt_template(file_path, isolated_scope)
311
+ return nil unless file_path && File.exist?(file_path)
312
+
313
+ ERB.new(File.read(file_path)).result(isolated_scope.instance_eval { binding })
314
+ end
315
+
316
+ def inflector_underscore(text)
317
+ return text.underscore if text.respond_to?(:underscore)
318
+
319
+ text
320
+ .to_s
321
+ .gsub("::", "/")
322
+ .gsub(/([A-Z]+)([A-Z][a-z])/, "\\1_\\2")
323
+ .gsub(/([a-z\d])([A-Z])/, "\\1_\\2")
324
+ .tr("-", "_")
325
+ .downcase
326
+ end
327
+ end
328
+ end
@@ -0,0 +1,10 @@
1
+ module OmniAgent
2
+ class Configuration
3
+ attr_accessor :default_provider, :default_model
4
+
5
+ def initialize
6
+ @default_provider = :openai
7
+ @default_model = "gpt-4o-mini"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ module OmniAgent
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace OmniAgent
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ module OmniAgent
2
+ class Error < StandardError; end
3
+ class MissingDependencyError < Error; end
4
+ end
@@ -0,0 +1,27 @@
1
+ # lib/omni_agents/providers/base.rb
2
+ module OmniAgent
3
+ module Providers
4
+ class Base
5
+ attr_reader :model
6
+
7
+ def initialize(api_key: nil, model: nil)
8
+ @api_key = api_key || default_api_key
9
+ @model = model || default_model
10
+ end
11
+
12
+ def chat(messages:, tools: [], **_options)
13
+ raise NotImplementedError, "Providers must implement #chat"
14
+ end
15
+
16
+ protected
17
+
18
+ def default_api_key
19
+ raise NotImplementedError, "Providers must define a default API key lookup"
20
+ end
21
+
22
+ def default_model
23
+ raise NotImplementedError, "Providers must define a default model"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,109 @@
1
+ # lib/omni_agent/providers/openai.rb
2
+ require 'json'
3
+
4
+ module OmniAgent
5
+ module Providers
6
+ class OpenAI < Base
7
+ begin
8
+ require 'openai'
9
+ rescue LoadError
10
+ raise OmniAgent::MissingDependencyError,
11
+ "The 'openai' gem is required to use the OpenAI provider. " \
12
+ "Please add `gem 'openai'` to your Gemfile."
13
+ end
14
+
15
+ def chat(messages:, tools: [], **options)
16
+ openai_tools = tools.map { |tool| format_tool(tool) }
17
+
18
+ payload = {
19
+ model: model,
20
+ messages: messages
21
+ }
22
+ payload[:tools] = openai_tools if openai_tools.any?
23
+ payload.merge!(options) if options.any?
24
+
25
+ response = client.chat.completions.create(**payload)
26
+
27
+ parse_response(response)
28
+ rescue => e
29
+ puts "Error during OpenAI chat: #{e.message}"
30
+ end
31
+
32
+ protected
33
+
34
+ def client
35
+ @client ||= ::OpenAI::Client.new(api_key: @api_key)
36
+ end
37
+
38
+ def default_api_key
39
+ ENV.fetch('OPENAI_ACCESS_TOKEN', nil)
40
+ end
41
+
42
+ def default_model
43
+ "gpt-4o-mini"
44
+ end
45
+
46
+ private
47
+
48
+ def format_tool(tool_class)
49
+ {
50
+ type: "function",
51
+ function: {
52
+ name: tool_class.name.split("::").last,
53
+ description: tool_class.description,
54
+ parameters: tool_class.json_schema
55
+ }
56
+ }
57
+ end
58
+
59
+ def parse_response(raw_response)
60
+ choices = raw_response.respond_to?(:choices) ? raw_response.choices : raw_response["choices"]
61
+ first_choice = choices&.first || {}
62
+
63
+ message = first_choice.respond_to?(:message) ? first_choice.message : (first_choice["message"] || {})
64
+ content = message.respond_to?(:content) ? message.content : message["content"]
65
+
66
+ raw_tool_calls = message.respond_to?(:tool_calls) ? message.tool_calls : message["tool_calls"]
67
+ raw_tool_calls ||= []
68
+
69
+ tool_calls = raw_tool_calls.map do |tc|
70
+ fn = tc.respond_to?(:function) ? tc.function : tc["function"]
71
+ {
72
+ id: tc.respond_to?(:id) ? tc.id : tc["id"],
73
+ name: fn.respond_to?(:name) ? fn.name : fn["name"],
74
+ arguments: JSON.parse((fn.respond_to?(:arguments) ? fn.arguments : fn["arguments"]) || "{}")
75
+ }
76
+ end
77
+
78
+ compat_tool_calls = raw_tool_calls.map do |tc|
79
+ fn = tc.respond_to?(:function) ? tc.function : tc["function"]
80
+ {
81
+ "id" => tc.respond_to?(:id) ? tc.id : tc["id"],
82
+ "type" => "function",
83
+ "function" => {
84
+ "name" => fn.respond_to?(:name) ? fn.name : fn["name"],
85
+ "arguments" => fn.respond_to?(:arguments) ? fn.arguments : fn["arguments"]
86
+ }
87
+ }
88
+ end.compact
89
+
90
+ compat_raw_response = {
91
+ "choices" => [
92
+ {
93
+ "message" => {
94
+ "content" => content,
95
+ "tool_calls" => (compat_tool_calls.empty? ? nil : compat_tool_calls)
96
+ }.compact
97
+ }
98
+ ]
99
+ }
100
+
101
+ OmniAgent::Providers::Response.new(
102
+ content: content,
103
+ raw_response: compat_raw_response,
104
+ tool_calls: tool_calls
105
+ )
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,17 @@
1
+ module OmniAgent
2
+ module Providers
3
+ class Response
4
+ attr_reader :content, :tool_calls, :raw_response
5
+
6
+ def initialize(content:, tool_calls: [], raw_response: nil)
7
+ @content = content
8
+ @tool_calls = tool_calls || []
9
+ @raw_response = raw_response
10
+ end
11
+
12
+ def tool_calls?
13
+ @tool_calls.any?
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,10 @@
1
+
2
+ module OmniAgent
3
+ module Providers
4
+ def self.registry
5
+ {
6
+ openai: OmniAgent::Providers::OpenAI
7
+ }
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,78 @@
1
+ require "fileutils"
2
+
3
+ namespace :omni_agent do
4
+ desc "Set up OmniAgent directories in your Rails app"
5
+ task install: :environment do
6
+ agents_root = Rails.root.join("app", "agents")
7
+ FileUtils.mkdir_p(agents_root)
8
+
9
+ puts "Created #{agents_root}"
10
+ puts "OmniAgent install complete."
11
+ puts "Next: bundle exec rake \"omni_agent:create_agent[ResearchAgent]\""
12
+ end
13
+
14
+ desc "Create an agent scaffold. Usage: rake \"omni_agent:create_agent[ResearchAgent]\" [-- --with-tools]"
15
+ task :create_agent, [:name] => :environment do |_task, args|
16
+ agent_name = args[:name].to_s.strip
17
+
18
+ if agent_name.empty?
19
+ abort "Please provide an agent name. Example: bundle exec rake \"omni_agent:create_agent[ResearchAgent]\""
20
+ end
21
+
22
+ with_tools = ARGV.include?("--with-tools")
23
+
24
+ agent_file_name = "#{agent_name.underscore}.rb"
25
+ agent_dir_name = agent_name.underscore
26
+ agent_dir = Rails.root.join("app", "agents", agent_dir_name)
27
+ tools_dir = agent_dir.join("tools")
28
+ prompt_file = agent_dir.join("prompt.md.erb")
29
+ agent_rb = agent_dir.join(agent_file_name)
30
+
31
+ FileUtils.mkdir_p(agent_dir)
32
+
33
+ if File.exist?(agent_rb)
34
+ abort "Agent file already exists: #{agent_rb}"
35
+ end
36
+
37
+ File.write(agent_rb, <<~RUBY)
38
+ class #{agent_name} < OmniAgent::Agent
39
+ end
40
+ RUBY
41
+
42
+ unless File.exist?(prompt_file)
43
+ File.write(prompt_file, <<~PROMPT)
44
+ You are #{agent_name}, a helpful assistant with access to local tools.
45
+ PROMPT
46
+ end
47
+
48
+ if with_tools
49
+ FileUtils.mkdir_p(tools_dir)
50
+
51
+ example_tool = tools_dir.join("example_tool.rb")
52
+ unless File.exist?(example_tool)
53
+ File.write(example_tool, <<~RUBY)
54
+ module #{agent_name}::Tools
55
+ class ExampleTool < OmniAgent::Tool
56
+ description "Example tool generated by omni_agent:create_agent"
57
+ tags :example
58
+ metadata category: :demo
59
+
60
+ input do
61
+ string :input_text, description: "Text to echo back"
62
+ end
63
+
64
+ def execute(input_text:)
65
+ "Echo: \#{input_text}"
66
+ end
67
+ end
68
+ end
69
+ RUBY
70
+ end
71
+ end
72
+
73
+ puts "Created #{agent_rb}"
74
+ puts "Created #{prompt_file}" if File.exist?(prompt_file)
75
+ puts "Created #{tools_dir}" if with_tools
76
+ puts "Created #{tools_dir.join("example_tool.rb")}" if with_tools && File.exist?(tools_dir.join("example_tool.rb"))
77
+ end
78
+ end
@@ -0,0 +1,61 @@
1
+ module OmniAgent
2
+ class Tool
3
+ class SchemaBuilder
4
+ attr_reader :properties, :required_fields
5
+
6
+ def initialize
7
+ @properties = {}
8
+ @required_fields = []
9
+ end
10
+
11
+ def string(name, description: nil, required: true)
12
+ add_property(name, type: "string", description: description, required: required)
13
+ end
14
+
15
+ def integer(name, description: nil, required: true)
16
+ add_property(name, type: "integer", description: description, required: required)
17
+ end
18
+
19
+ def boolean(name, description: nil, required: true)
20
+ add_property(name, type: "boolean", description: description, required: required)
21
+ end
22
+
23
+ def array(name, items_type:, description: nil, required: true)
24
+ property = { type: "array", items: { type: items_type } }
25
+ property[:description] = description if description
26
+
27
+ @properties[name] = property
28
+ @required_fields << name.to_s if required
29
+ end
30
+
31
+ def hash(name, description: nil, required: true, &block)
32
+ property = { type: "object" }
33
+ property[:description] = description if description
34
+
35
+ if block_given?
36
+ nested_builder = SchemaBuilder.new
37
+ nested_builder.instance_eval(&block)
38
+
39
+ property[:properties] = nested_builder.properties
40
+ property[:required] = nested_builder.required_fields
41
+ property[:additionalProperties] = false
42
+ else
43
+ property[:additionalProperties] = true
44
+ end
45
+
46
+ @properties[name] = property
47
+ @required_fields << name.to_s if required
48
+ end
49
+
50
+ private
51
+
52
+ def add_property(name, type:, description:, required:)
53
+ property = { type: type }
54
+ property[:description] = description if description
55
+
56
+ @properties[name] = property
57
+ @required_fields << name.to_s if required
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,68 @@
1
+ # lib/omni_agents/tool.rb
2
+ module OmniAgent
3
+ class Tool
4
+ class << self
5
+ def description(text = nil)
6
+ @description = text if text
7
+ @description || "No description provided."
8
+ end
9
+
10
+ def metadata(options = nil)
11
+ @metadata = options if options
12
+ @metadata || {}
13
+ end
14
+
15
+ def tags(*tag_names)
16
+ return @tags || [] if tag_names.empty?
17
+
18
+ @tags = (tags + normalize_tags(tag_names)).uniq
19
+ end
20
+
21
+ def configured_tags
22
+ tags
23
+ end
24
+
25
+ def input(&block)
26
+ if block_given?
27
+ builder = SchemaBuilder.new
28
+ builder.instance_eval(&block)
29
+
30
+ @properties = builder.properties
31
+ @required = builder.required_fields
32
+ end
33
+ end
34
+
35
+ def json_schema
36
+ {
37
+ type: "object",
38
+ properties: @properties || {},
39
+ required: @required || [],
40
+ additionalProperties: false
41
+ }
42
+ end
43
+
44
+ def invoke(arguments_hash)
45
+ kwargs = arguments_hash.transform_keys(&:to_sym)
46
+ new.execute(**kwargs)
47
+ end
48
+
49
+ private
50
+
51
+ def normalize_tags(tag_names)
52
+ raise ArgumentError, "tags requires at least one tag" if tag_names.empty?
53
+
54
+ tag_names.map do |tag_name|
55
+ unless tag_name.is_a?(String) || tag_name.is_a?(Symbol)
56
+ raise ArgumentError, "tags must be strings or symbols"
57
+ end
58
+
59
+ tag_name.to_sym
60
+ end
61
+ end
62
+ end
63
+
64
+ def execute(**args)
65
+ raise NotImplementedError, "#{self.class.name} must implement #execute"
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,3 @@
1
+ module OmniAgent
2
+ VERSION = "0.1.0"
3
+ end
data/lib/omni_agent.rb ADDED
@@ -0,0 +1,21 @@
1
+ require "omni_agent/version"
2
+ require "omni_agent/engine" if defined?(Rails)
3
+ require "zeitwerk"
4
+
5
+ loader = Zeitwerk::Loader.for_gem
6
+ loader.inflector.inflect("openai" => "OpenAI")
7
+ loader.ignore(File.expand_path("generators", __dir__))
8
+ loader.setup
9
+
10
+
11
+ module OmniAgent
12
+ class << self
13
+ def configuration
14
+ @configuration ||= Configuration.new
15
+ end
16
+
17
+ def configure
18
+ yield(configuration)
19
+ end
20
+ end
21
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omni_agent
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - ACR1209
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 8.1.3
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 8.1.3
26
+ - !ruby/object:Gem::Dependency
27
+ name: zeitwerk
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.6'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.6'
40
+ description: OmniAgent provides a Rails-native framework for defining AI agents, tool
41
+ schemas, prompt templates, callbacks, and provider-backed generation workflows.
42
+ email:
43
+ - andrescoronel1209@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - CHANGELOG.md
49
+ - MIT-LICENSE
50
+ - README.md
51
+ - Rakefile
52
+ - app/assets/stylesheets/omni_agent/application.css
53
+ - app/controllers/omni_agent/application_controller.rb
54
+ - app/helpers/omni_agent/application_helper.rb
55
+ - app/jobs/omni_agent/application_job.rb
56
+ - app/mailers/omni_agent/application_mailer.rb
57
+ - app/models/omni_agent/application_record.rb
58
+ - app/views/layouts/omni_agent/application.html.erb
59
+ - config/routes.rb
60
+ - lib/generators/omni_agent/agent/agent_generator.rb
61
+ - lib/generators/omni_agent/install/install_generator.rb
62
+ - lib/omni_agent.rb
63
+ - lib/omni_agent/agent.rb
64
+ - lib/omni_agent/configuration.rb
65
+ - lib/omni_agent/engine.rb
66
+ - lib/omni_agent/errors.rb
67
+ - lib/omni_agent/providers.rb
68
+ - lib/omni_agent/providers/base.rb
69
+ - lib/omni_agent/providers/openai.rb
70
+ - lib/omni_agent/providers/response.rb
71
+ - lib/omni_agent/tasks/omni_agent_tasks.rake
72
+ - lib/omni_agent/tool.rb
73
+ - lib/omni_agent/tool/schema_builder.rb
74
+ - lib/omni_agent/version.rb
75
+ homepage: https://github.com/ACR1209/omni_agent
76
+ licenses:
77
+ - MIT
78
+ metadata:
79
+ homepage_uri: https://github.com/ACR1209/omni_agent
80
+ source_code_uri: https://github.com/ACR1209/omni_agent
81
+ changelog_uri: https://github.com/ACR1209/omni_agent/blob/main/CHANGELOG.md
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubygems_version: 3.6.9
97
+ specification_version: 4
98
+ summary: Rails engine for building AI agents with tools.
99
+ test_files: []