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 +7 -0
- data/CHANGELOG.md +15 -0
- data/MIT-LICENSE +20 -0
- data/README.md +123 -0
- data/Rakefile +11 -0
- data/app/assets/stylesheets/omni_agent/application.css +15 -0
- data/app/controllers/omni_agent/application_controller.rb +4 -0
- data/app/helpers/omni_agent/application_helper.rb +4 -0
- data/app/jobs/omni_agent/application_job.rb +4 -0
- data/app/mailers/omni_agent/application_mailer.rb +6 -0
- data/app/models/omni_agent/application_record.rb +5 -0
- data/app/views/layouts/omni_agent/application.html.erb +17 -0
- data/config/routes.rb +2 -0
- data/lib/generators/omni_agent/agent/agent_generator.rb +103 -0
- data/lib/generators/omni_agent/install/install_generator.rb +21 -0
- data/lib/omni_agent/agent.rb +328 -0
- data/lib/omni_agent/configuration.rb +10 -0
- data/lib/omni_agent/engine.rb +5 -0
- data/lib/omni_agent/errors.rb +4 -0
- data/lib/omni_agent/providers/base.rb +27 -0
- data/lib/omni_agent/providers/openai.rb +109 -0
- data/lib/omni_agent/providers/response.rb +17 -0
- data/lib/omni_agent/providers.rb +10 -0
- data/lib/omni_agent/tasks/omni_agent_tasks.rake +78 -0
- data/lib/omni_agent/tool/schema_builder.rb +61 -0
- data/lib/omni_agent/tool.rb +68 -0
- data/lib/omni_agent/version.rb +3 -0
- data/lib/omni_agent.rb +21 -0
- metadata +99 -0
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,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,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,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,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,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
|
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: []
|