omni_agent 0.1.8 → 0.1.10
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 +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +74 -1
- data/exe/omni_agent +22 -6
- data/lib/generators/omni_agent/agent/agent_generator.rb +1 -0
- data/lib/generators/omni_agent/install/install_generator.rb +1 -0
- data/lib/generators/omni_agent/mcp_server/mcp_server_generator.rb +151 -0
- data/lib/omni_agent/agent.rb +51 -36
- data/lib/omni_agent/configuration.rb +4 -1
- data/lib/omni_agent/engine.rb +4 -0
- data/lib/omni_agent/errors.rb +2 -0
- data/lib/omni_agent/mcp/authenticator.rb +82 -0
- data/lib/omni_agent/mcp/handler.rb +128 -0
- data/lib/omni_agent/mcp/rack_app.rb +107 -0
- data/lib/omni_agent/mcp/request.rb +24 -0
- data/lib/omni_agent/mcp/routing.rb +57 -0
- data/lib/omni_agent/mcp/server.rb +238 -0
- data/lib/omni_agent/mcp/stdio.rb +56 -0
- data/lib/omni_agent/mcp/tool_adapter.rb +45 -0
- data/lib/omni_agent/mcp.rb +31 -0
- data/lib/omni_agent/providers/base.rb +1 -1
- data/lib/omni_agent/providers/mock.rb +8 -1
- data/lib/omni_agent/providers/ollama.rb +30 -0
- data/lib/omni_agent/providers/openai.rb +23 -3
- data/lib/omni_agent/providers.rb +1 -0
- data/lib/omni_agent/streaming/event.rb +54 -0
- data/lib/omni_agent/streaming/proxy.rb +17 -0
- data/lib/omni_agent/tool/schema_builder.rb +130 -10
- data/lib/omni_agent/tool.rb +117 -1
- data/lib/omni_agent/version.rb +1 -1
- data/lib/omni_agent.rb +1 -1
- data/lib/tasks/omni_agent_mcp_tasks.rake +9 -0
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f8a84d4b38e835bc00b73a88bc233932198498112e3837e33fdfdf3dbabab981
|
|
4
|
+
data.tar.gz: 6d5cd7709dc61be01ae892a67732b08ee3e17a6d4af7d81f8640f0fd618010e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b56c77174212b7a4ba2e45b2b3bb6e0f384631fcdd2bcbf30e2a32c8edb1617b239e8bdd482dbb259baf48746b20c38d29fd325685262337e38ab1e9669a5220
|
|
7
|
+
data.tar.gz: 5fcb5179527eda96d3dc6726ad78c73b1bba4b3ef7c34825cbfec03c2437e7018647b43cf9f9d43791599efdc628530d5c6cfefcb6f431bd7548a97f7c9a8365
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.1.10](https://github.com/ACR1209/omni_agent/compare/omni_agent/v0.1.9...omni_agent/v0.1.10) (2026-10-09)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **mcp:** expose tools and agents as MCP servers ([#40](https://github.com/ACR1209/omni_agent/issues/40)) ([db8dcb8](https://github.com/ACR1209/omni_agent/commit/db8dcb8776cc0baaed45112b61bc329f03d32181))
|
|
11
|
+
|
|
12
|
+
## [0.1.9](https://github.com/ACR1209/omni_agent/compare/omni_agent/v0.1.8...omni_agent/v0.1.9) (2026-07-12)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **agent:** support streaming responses via stream sink ([7cf1fba](https://github.com/ACR1209/omni_agent/commit/7cf1fba4fa378c5cee7f464354f760506a0ff513))
|
|
18
|
+
* **docs:** add enum type for temperature unit in input schema ([d992658](https://github.com/ACR1209/omni_agent/commit/d99265836307ce8f863b060eff73c7acdbc8d2ff))
|
|
19
|
+
* **providers:** add Ollama provider via OpenAI-compat endpoint ([4ae92ad](https://github.com/ACR1209/omni_agent/commit/4ae92ad0f549119b43c2e8a7381f4f7245aa2f68))
|
|
20
|
+
* **tool:** add optional constraint validations and custom validate procs ([db8371a](https://github.com/ACR1209/omni_agent/commit/db8371a189f710e812c3c3ce15f200df1ba94902))
|
|
21
|
+
* **tool:** add polymorphic reference datatype ([1686bbb](https://github.com/ACR1209/omni_agent/commit/1686bbbf41932622a5537c335da8c41c68a73f73))
|
|
22
|
+
* **tool:** infer and validate enum value types in schema builder ([ceb3e78](https://github.com/ACR1209/omni_agent/commit/ceb3e780498caa1158e9fc8af3eb6e9b95c33ddb))
|
|
23
|
+
* **tool:** validate required and enum arguments on invoke ([543c6e0](https://github.com/ACR1209/omni_agent/commit/543c6e08f9c394b84c73bc5957aff7058e0fab19))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
* **lint:** add space inside empty block braces ([9116b3f](https://github.com/ACR1209/omni_agent/commit/9116b3ff82e9fab9824347515b4c1cd40a40785e))
|
|
29
|
+
* **spec:** redact OpenAI API key from recorded VCR cassettes ([53cecd5](https://github.com/ACR1209/omni_agent/commit/53cecd540b6dd52ddc5bf3da0550836fbfcfab88))
|
|
30
|
+
|
|
5
31
|
## [0.1.8](https://github.com/ACR1209/omni_agent/compare/omni_agent/v0.1.7...omni_agent/v0.1.8) (2026-07-07)
|
|
6
32
|
|
|
7
33
|
|
data/README.md
CHANGED
|
@@ -11,7 +11,8 @@ tool schemas, and generation lifecycle callbacks.
|
|
|
11
11
|
- Prompt composition from ERB files in `app/agents/<agent_name>/`
|
|
12
12
|
- Agent callbacks (`before_generation`, `after_generation`)
|
|
13
13
|
- Agent and tool tags to support filtering strategies
|
|
14
|
-
- OpenAI provider integration out of the box
|
|
14
|
+
- OpenAI provider integration out of the box, plus an Ollama provider for local models
|
|
15
|
+
- MCP server support: expose tools and whole agents to Claude Code, Claude Desktop, Cursor and other MCP clients
|
|
15
16
|
- Rake tasks and Rails generators for scaffolding
|
|
16
17
|
|
|
17
18
|
## Installation
|
|
@@ -27,6 +28,8 @@ Add the provider you're using to the Gemfile as well:
|
|
|
27
28
|
gem "openai"
|
|
28
29
|
```
|
|
29
30
|
|
|
31
|
+
The `ollama` provider also depends on the `openai` gem — it talks to Ollama's OpenAI-compatible endpoint, so no separate gem is needed.
|
|
32
|
+
|
|
30
33
|
Then run:
|
|
31
34
|
|
|
32
35
|
```bash
|
|
@@ -53,6 +56,14 @@ bundle exec rails generate omni_agent:agent ResearchAgent --model gpt-4.1-mini -
|
|
|
53
56
|
OPENAI_ACCESS_TOKEN=your_api_key_here
|
|
54
57
|
```
|
|
55
58
|
|
|
59
|
+
To use Ollama instead, set `config.default_provider = :ollama` (see below) and, if needed, override its endpoint/model:
|
|
60
|
+
|
|
61
|
+
```dotenv
|
|
62
|
+
OLLAMA_HOST=http://localhost:11434 # defaults to this
|
|
63
|
+
OLLAMA_MODEL=llama3.1 # defaults to this; pick a tool-calling-capable model
|
|
64
|
+
OLLAMA_API_KEY=ollama # unused by Ollama, but required by the openai client
|
|
65
|
+
```
|
|
66
|
+
|
|
56
67
|
4. Implement your agent prompt and optional tools under:
|
|
57
68
|
|
|
58
69
|
```text
|
|
@@ -129,6 +140,23 @@ class SupervisorAgent < OmniAgent::Agent
|
|
|
129
140
|
end
|
|
130
141
|
```
|
|
131
142
|
|
|
143
|
+
## Streaming
|
|
144
|
+
|
|
145
|
+
Prefix any run entrypoint with `.stream` and pass a block to receive the response as it's generated instead of waiting for the full result:
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
ResearchAgent.with(user_id: 42).stream.run("What's new?") do |event|
|
|
149
|
+
case event.type
|
|
150
|
+
when :text then print event.text
|
|
151
|
+
when :tool_call then puts "\n[using #{event.tool_name}...]"
|
|
152
|
+
when :tool_result then puts "[#{event.error? ? "failed" : "done"}]"
|
|
153
|
+
when :done then puts "\n---"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`.stream` must come before the call (`.stream.run(...)`, not `.run(...).stream`) — without it, or without a block, behavior is unchanged and the same `Response` is returned either way. Only the `openai`, `ollama`, and `mock` providers stream today. See [Streaming Responses](omniagent-docs/docs/agent/streaming.mdx) for the full event reference.
|
|
159
|
+
|
|
132
160
|
## Evals
|
|
133
161
|
|
|
134
162
|
`OmniAgent::Eval` lets you test agent quality: deterministic assertions (tool calls, output matching) and pluggable LLM-as-judge scoring.
|
|
@@ -213,6 +241,51 @@ There's also an equivalent `rake omni_agent:eval` task (`rake "omni_agent:eval[p
|
|
|
213
241
|
|
|
214
242
|
Calls real LLM providers (cost, non-determinism) — deliberately **not** part of `bundle exec rspec` or CI.
|
|
215
243
|
|
|
244
|
+
## MCP Servers
|
|
245
|
+
|
|
246
|
+
Expose your tools (and whole agents) to MCP clients such as Claude Code, Claude Desktop and Cursor. The protocol is implemented natively; no extra gem is needed.
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
rails generate omni_agent:mcp_server Support --with-tools LookupOrder --agents SupportAgent
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
# app/mcp_servers/support_server.rb
|
|
254
|
+
class SupportServer < OmniAgent::MCP::Server
|
|
255
|
+
instructions "Tools for the support team."
|
|
256
|
+
|
|
257
|
+
# Tools in app/mcp_servers/support_server/tools/ (SupportServer::Tools::*) are added automatically.
|
|
258
|
+
# Reuse existing tools and agents too:
|
|
259
|
+
tools ResearchAgent::Tools::GetWeather
|
|
260
|
+
tools_from SupportAgent
|
|
261
|
+
expose_agent ResearchAgent, as: :research, description: "Ask the research agent", forward: [ :current_user ]
|
|
262
|
+
|
|
263
|
+
authenticate :bearer, tokens: -> { ENV.fetch("SUPPORT_MCP_TOKENS", "").split(",") }
|
|
264
|
+
# or: authenticate { |request| User.find_by(api_token: request.bearer_token) }
|
|
265
|
+
|
|
266
|
+
authorize_tool { |tool_class, principal| !tool_class.tags.include?(:admin) }
|
|
267
|
+
context { |principal, _request| { current_user: principal } }
|
|
268
|
+
end
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
# config/routes.rb -- Streamable HTTP (stateless, POST only)
|
|
273
|
+
mcp_server :support # SupportServer at /mcp/support
|
|
274
|
+
mcp_server :billing, path: "/internal/mcp"
|
|
275
|
+
# or mount every server in app/mcp_servers at /mcp/<name>:
|
|
276
|
+
mcp_servers
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
# stdio, for local clients
|
|
281
|
+
bundle exec omni_agent mcp SupportServer
|
|
282
|
+
|
|
283
|
+
claude mcp add --transport http support http://localhost:3000/mcp/support --header "Authorization: Bearer $TOKEN"
|
|
284
|
+
claude mcp add support -- bundle exec omni_agent mcp SupportServer
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Tools can declare MCP-only hints with `title "Get weather"` and `annotations read_only: true, destructive: false, idempotent: true, open_world: true`. HTTP servers must declare `authenticate` (use `authenticate :none` to opt out on purpose). Requests from browser origins that are not in `allowed_origins` / `config.mcp_allowed_origins` are rejected.
|
|
288
|
+
|
|
216
289
|
## Configuration
|
|
217
290
|
|
|
218
291
|
Global defaults can be configured through `OmniAgent.configure`:
|
data/exe/omni_agent
CHANGED
|
@@ -5,10 +5,21 @@ require "optparse"
|
|
|
5
5
|
|
|
6
6
|
options = { fresh: false }
|
|
7
7
|
parser = OptionParser.new do |opts|
|
|
8
|
-
opts.banner =
|
|
8
|
+
opts.banner = <<~BANNER
|
|
9
|
+
Usage: omni_agent eval [pattern] [options]
|
|
10
|
+
omni_agent mcp <ServerClass>
|
|
11
|
+
BANNER
|
|
9
12
|
opts.on("--fresh", "Clear the eval cache before running") { options[:fresh] = true }
|
|
10
13
|
end
|
|
11
14
|
|
|
15
|
+
def require_rails_environment(command)
|
|
16
|
+
environment_file = File.expand_path("config/environment", Dir.pwd)
|
|
17
|
+
unless File.exist?("#{environment_file}.rb")
|
|
18
|
+
abort "Could not find config/environment.rb -- run `omni_agent #{command}` from your Rails app root."
|
|
19
|
+
end
|
|
20
|
+
require environment_file
|
|
21
|
+
end
|
|
22
|
+
|
|
12
23
|
command = ARGV.shift
|
|
13
24
|
|
|
14
25
|
case command
|
|
@@ -16,11 +27,7 @@ when "eval"
|
|
|
16
27
|
parser.parse!(ARGV)
|
|
17
28
|
pattern = ARGV.shift
|
|
18
29
|
|
|
19
|
-
|
|
20
|
-
unless File.exist?("#{environment_file}.rb")
|
|
21
|
-
abort "Could not find config/environment.rb -- run `omni_agent eval` from your Rails app root."
|
|
22
|
-
end
|
|
23
|
-
require environment_file
|
|
30
|
+
require_rails_environment("eval")
|
|
24
31
|
|
|
25
32
|
status = OmniAgent::Eval::CLI.run(pattern: pattern, fresh: options[:fresh])
|
|
26
33
|
|
|
@@ -32,6 +39,15 @@ when "eval"
|
|
|
32
39
|
when :failed
|
|
33
40
|
exit 1
|
|
34
41
|
end
|
|
42
|
+
when "mcp"
|
|
43
|
+
server = ARGV.shift.to_s.strip
|
|
44
|
+
if server.empty?
|
|
45
|
+
warn "Missing server class. Example: omni_agent mcp SupportServer"
|
|
46
|
+
exit 1
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
require_rails_environment("mcp")
|
|
50
|
+
OmniAgent::MCP::Stdio.start(server)
|
|
35
51
|
when nil
|
|
36
52
|
warn parser.banner
|
|
37
53
|
exit 1
|
|
@@ -15,6 +15,7 @@ module OmniAgent
|
|
|
15
15
|
def show_next_steps
|
|
16
16
|
say "OmniAgent install complete."
|
|
17
17
|
say "Next: rails generate omni_agent:agent SupportAgent --with-tools Tool1 Tool2"
|
|
18
|
+
say "Expose tools to MCP clients: rails generate omni_agent:mcp_server Support --agents SupportAgent"
|
|
18
19
|
end
|
|
19
20
|
end
|
|
20
21
|
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
require "rails/generators/named_base"
|
|
2
|
+
|
|
3
|
+
module OmniAgent
|
|
4
|
+
module Generators
|
|
5
|
+
class McpServerGenerator < Rails::Generators::NamedBase
|
|
6
|
+
desc "Creates an OmniAgent MCP server in app/mcp_servers and mounts it in config/routes.rb"
|
|
7
|
+
|
|
8
|
+
class_option :with_tools,
|
|
9
|
+
type: :array,
|
|
10
|
+
default: nil,
|
|
11
|
+
lazy_default: [],
|
|
12
|
+
banner: "[Tool1 Tool2 ...]",
|
|
13
|
+
desc: "Scaffold server-local tools in app/mcp_servers/<name>_server/tools. Optionally pass tool names"
|
|
14
|
+
class_option :tools,
|
|
15
|
+
type: :array,
|
|
16
|
+
default: [],
|
|
17
|
+
banner: "[ResearchAgent::Tools::GetWeather ...]",
|
|
18
|
+
desc: "Existing tool classes to expose"
|
|
19
|
+
class_option :agents,
|
|
20
|
+
type: :array,
|
|
21
|
+
default: [],
|
|
22
|
+
banner: "[ResearchAgent ...]",
|
|
23
|
+
desc: "Agents whose tools are all exposed (tools_from)"
|
|
24
|
+
class_option :path,
|
|
25
|
+
type: :string,
|
|
26
|
+
default: nil,
|
|
27
|
+
desc: "Mount path for the HTTP endpoint (default: /mcp/<name>)"
|
|
28
|
+
|
|
29
|
+
def create_server_file
|
|
30
|
+
create_file(File.join("app", "mcp_servers", "#{server_file_name}.rb"), server_template)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def create_tools_if_requested
|
|
34
|
+
return if options[:with_tools].nil?
|
|
35
|
+
|
|
36
|
+
requested_tool_names.each do |tool_name|
|
|
37
|
+
create_file(File.join("app", "mcp_servers", server_file_name, "tools", "#{tool_name.underscore}.rb"), tool_template(tool_name))
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def mount_route
|
|
42
|
+
if mounts_all_servers?
|
|
43
|
+
say_status :skip, "config/routes.rb already mounts every server with `mcp_servers`", :yellow
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
route_line = "mcp_server #{server_slug.include?('/') ? server_slug.inspect : ":#{server_slug}"}"
|
|
48
|
+
route_line += %(, path: "#{options[:path]}") if options[:path].present?
|
|
49
|
+
route route_line
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def show_next_steps
|
|
53
|
+
say ""
|
|
54
|
+
say "MCP server #{server_class_name} created."
|
|
55
|
+
say ""
|
|
56
|
+
say "Set a token, start the app and connect Claude Code over HTTP:"
|
|
57
|
+
say " #{token_env_var}=change-me bin/rails server"
|
|
58
|
+
say " claude mcp add --transport http #{server_identifier} http://localhost:3000#{mount_path} --header \"Authorization: Bearer change-me\""
|
|
59
|
+
say ""
|
|
60
|
+
say "Or run it locally over stdio (no HTTP auth; see stdio_principal):"
|
|
61
|
+
say " claude mcp add #{server_identifier} -- bundle exec omni_agent mcp #{server_class_name}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def server_class_name
|
|
67
|
+
class_name.end_with?("Server") ? class_name : "#{class_name}Server"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def server_file_name
|
|
71
|
+
server_class_name.underscore
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def server_slug
|
|
75
|
+
OmniAgent::MCP::Routing.slug_for(server_class_name)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def server_identifier
|
|
79
|
+
server_slug.tr("/", "_")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def mount_path
|
|
83
|
+
options[:path].presence || "#{OmniAgent::MCP::Routing::DEFAULT_PATH_PREFIX}/#{server_slug.dasherize}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def mounts_all_servers?
|
|
87
|
+
routes_file = File.join(destination_root, "config", "routes.rb")
|
|
88
|
+
File.exist?(routes_file) && File.read(routes_file).match?(/^\s*mcp_servers\s*(\(\s*\)|except:.*)?\s*$/)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def token_env_var
|
|
92
|
+
"#{server_identifier.upcase}_MCP_TOKENS"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def server_template
|
|
96
|
+
<<~RUBY
|
|
97
|
+
class #{server_class_name} < OmniAgent::MCP::Server
|
|
98
|
+
instructions "Tools exposed by #{server_class_name}."
|
|
99
|
+
|
|
100
|
+
#{tool_lines.map { |line| " #{line}" }.join("\n")}
|
|
101
|
+
|
|
102
|
+
# Comma-separated list of accepted bearer tokens.
|
|
103
|
+
authenticate :bearer, tokens: -> { ENV.fetch("#{token_env_var}", "").split(",") }
|
|
104
|
+
|
|
105
|
+
# Hide tools per principal:
|
|
106
|
+
# authorize_tool { |tool_class, principal| true }
|
|
107
|
+
|
|
108
|
+
# Extra values merged into each tool's `context`:
|
|
109
|
+
# context { |principal, request| { current_user: principal } }
|
|
110
|
+
end
|
|
111
|
+
RUBY
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def requested_tool_names
|
|
115
|
+
names = Array(options[:with_tools]).map(&:strip).reject(&:empty?)
|
|
116
|
+
names.empty? ? [ "ExampleTool" ] : names.map(&:camelize)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def tool_template(tool_name)
|
|
120
|
+
<<~RUBY
|
|
121
|
+
module #{server_class_name}::Tools
|
|
122
|
+
class #{tool_name} < OmniAgent::Tool
|
|
123
|
+
description "#{tool_name} generated by omni_agent:mcp_server"
|
|
124
|
+
annotations read_only: true
|
|
125
|
+
|
|
126
|
+
input do
|
|
127
|
+
string :input_text, description: "Text input"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def execute(input_text:)
|
|
131
|
+
"Echo: \#{input_text}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
RUBY
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def tool_lines
|
|
139
|
+
tools = Array(options[:tools]).map(&:strip).reject(&:empty?)
|
|
140
|
+
agents = Array(options[:agents]).map(&:strip).reject(&:empty?)
|
|
141
|
+
|
|
142
|
+
lines = []
|
|
143
|
+
lines << "tools #{tools.join(', ')}" if tools.any?
|
|
144
|
+
lines.concat(agents.map { |agent| "tools_from #{agent}" })
|
|
145
|
+
lines.unshift("# Tools in app/mcp_servers/#{server_file_name}/tools/ are added automatically.")
|
|
146
|
+
lines << "# tools SomeAgent::Tools::SomeTool" if tools.empty? && agents.empty?
|
|
147
|
+
lines
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
data/lib/omni_agent/agent.rb
CHANGED
|
@@ -20,7 +20,7 @@ module OmniAgent
|
|
|
20
20
|
@_omni_agent_wrapping_method = true
|
|
21
21
|
alias_method alias_name, method_name
|
|
22
22
|
|
|
23
|
-
define_method(method_name) do |input = nil, context: {}, **context_keywords|
|
|
23
|
+
define_method(method_name) do |input = nil, context: {}, **context_keywords, &block|
|
|
24
24
|
if input.nil? && context == {} && context_keywords.empty?
|
|
25
25
|
run_alias_entrypoint_logic(alias_name)
|
|
26
26
|
else
|
|
@@ -29,7 +29,7 @@ module OmniAgent
|
|
|
29
29
|
|
|
30
30
|
run_input = run_alias_entrypoint_logic(alias_name, fallback_input: input)
|
|
31
31
|
|
|
32
|
-
run(run_input, context: merged_context, prompt_method: method_name)
|
|
32
|
+
run(run_input, context: merged_context, prompt_method: method_name, &block)
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
ensure
|
|
@@ -78,8 +78,8 @@ module OmniAgent
|
|
|
78
78
|
aliases = normalize_callbacks(:run_aliases, method_names)
|
|
79
79
|
|
|
80
80
|
aliases.each do |method_name|
|
|
81
|
-
define_method(method_name) do |input, context: {}|
|
|
82
|
-
run(input, context: context, prompt_method: method_name)
|
|
81
|
+
define_method(method_name) do |input, context: {}, &block|
|
|
82
|
+
run(input, context: context, prompt_method: method_name, &block)
|
|
83
83
|
end
|
|
84
84
|
end
|
|
85
85
|
end
|
|
@@ -99,6 +99,38 @@ module OmniAgent
|
|
|
99
99
|
@delegated_tool_classes || []
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
+
def tool_classes
|
|
103
|
+
tool_namespace = "#{name}::Tools".safe_constantize
|
|
104
|
+
|
|
105
|
+
namespace_tools = if tool_namespace
|
|
106
|
+
tool_namespace.constants.filter_map do |const_name|
|
|
107
|
+
const = tool_namespace.const_get(const_name)
|
|
108
|
+
const if const.is_a?(Class) && const < OmniAgent::Tool
|
|
109
|
+
end
|
|
110
|
+
else
|
|
111
|
+
[]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
namespace_tools + configured_delegated_tool_classes
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def build_delegated_tool_class(agent_class, description:, run_alias:, forward:)
|
|
118
|
+
tool_description = description || "Delegate to #{agent_class.name}."
|
|
119
|
+
|
|
120
|
+
Class.new(OmniAgent::Tool) do
|
|
121
|
+
description tool_description
|
|
122
|
+
|
|
123
|
+
input do
|
|
124
|
+
string :input, description: "Input/question to send to the delegated agent."
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
define_method(:execute) do |input:|
|
|
128
|
+
forwarded_context = OmniAgent::Agent.__send__(:filter_forwarded_context, context, forward)
|
|
129
|
+
OmniAgent::Agent.__send__(:run_delegated_agent, agent_class, input, run_alias, forwarded_context)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
102
134
|
def with(context = nil, provider_override: nil, model_override: nil, options_override: {}, **context_keywords)
|
|
103
135
|
merged_context = {}
|
|
104
136
|
merged_context.merge!(context) if context.is_a?(Hash)
|
|
@@ -135,23 +167,6 @@ module OmniAgent
|
|
|
135
167
|
as.to_s.split(/[_\s]+/).reject(&:empty?).map { |part| part[0].upcase + part[1..] }.join
|
|
136
168
|
end
|
|
137
169
|
|
|
138
|
-
def build_delegated_tool_class(agent_class, description:, run_alias:, forward:)
|
|
139
|
-
tool_description = description || "Delegate to #{agent_class.name}."
|
|
140
|
-
|
|
141
|
-
Class.new(OmniAgent::Tool) do
|
|
142
|
-
description tool_description
|
|
143
|
-
|
|
144
|
-
input do
|
|
145
|
-
string :input, description: "Input/question to send to the delegated agent."
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
define_method(:execute) do |input:|
|
|
149
|
-
forwarded_context = OmniAgent::Agent.__send__(:filter_forwarded_context, context, forward)
|
|
150
|
-
OmniAgent::Agent.__send__(:run_delegated_agent, agent_class, input, run_alias, forwarded_context)
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
170
|
def filter_forwarded_context(context, forward)
|
|
156
171
|
return {} unless context.is_a?(Hash)
|
|
157
172
|
return context.dup if forward == true
|
|
@@ -212,7 +227,11 @@ module OmniAgent
|
|
|
212
227
|
@default_context = context_override || {}
|
|
213
228
|
end
|
|
214
229
|
|
|
215
|
-
def
|
|
230
|
+
def stream
|
|
231
|
+
OmniAgent::Streaming::Proxy.new(self)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def run(input, context: {}, prompt_method: nil, &stream)
|
|
216
235
|
context = @default_context.merge(context || {})
|
|
217
236
|
bind_context_instance_variables(context)
|
|
218
237
|
|
|
@@ -237,13 +256,14 @@ module OmniAgent
|
|
|
237
256
|
"Increase OmniAgent.configuration.max_tool_iterations if more tool calls are expected."
|
|
238
257
|
end
|
|
239
258
|
|
|
240
|
-
response = provider.chat(messages: messages, tools: filtered_tools, **@chat_options)
|
|
259
|
+
response = provider.chat(messages: messages, tools: filtered_tools, stream: stream, **@chat_options)
|
|
241
260
|
|
|
242
261
|
if response.content && !response.tool_calls?
|
|
243
262
|
messages << { role: "assistant", content: response.content }
|
|
244
263
|
set_after_generation_state(response: response, messages: messages, initial_messages_count: initial_messages_count)
|
|
245
264
|
run_after_generation_callbacks(input: input, context: context, messages: messages, response: response)
|
|
246
265
|
sync_context_from_instance_variables(context)
|
|
266
|
+
stream&.call(OmniAgent::Streaming::Event.done(response))
|
|
247
267
|
return response
|
|
248
268
|
end
|
|
249
269
|
|
|
@@ -265,6 +285,8 @@ module OmniAgent
|
|
|
265
285
|
tool_instance = tool_class.new
|
|
266
286
|
tool_instance.context = context if tool_instance.respond_to?(:context=)
|
|
267
287
|
|
|
288
|
+
stream&.call(OmniAgent::Streaming::Event.tool_call(name: tool_name, arguments: tool_args, id: tool_id))
|
|
289
|
+
|
|
268
290
|
begin
|
|
269
291
|
result = tool_instance.invoke(tool_args)
|
|
270
292
|
|
|
@@ -274,13 +296,16 @@ module OmniAgent
|
|
|
274
296
|
name: tool_name,
|
|
275
297
|
content: result.to_s
|
|
276
298
|
}
|
|
299
|
+
stream&.call(OmniAgent::Streaming::Event.tool_result(name: tool_name, id: tool_id, content: result.to_s))
|
|
277
300
|
rescue => e
|
|
301
|
+
error_content = "Error executing tool: #{e.message}"
|
|
278
302
|
messages << {
|
|
279
303
|
role: "tool",
|
|
280
304
|
tool_call_id: tool_id,
|
|
281
305
|
name: tool_name,
|
|
282
|
-
content:
|
|
306
|
+
content: error_content
|
|
283
307
|
}
|
|
308
|
+
stream&.call(OmniAgent::Streaming::Event.tool_result(name: tool_name, id: tool_id, content: error_content, error: true))
|
|
284
309
|
end
|
|
285
310
|
|
|
286
311
|
should_stop_generation ||= tool_class.respond_to?(:stops_generation?) && tool_class.stops_generation?
|
|
@@ -299,24 +324,14 @@ module OmniAgent
|
|
|
299
324
|
set_after_generation_state(response: response, messages: messages, initial_messages_count: initial_messages_count)
|
|
300
325
|
run_after_generation_callbacks(input: input, context: context, messages: messages, response: response)
|
|
301
326
|
sync_context_from_instance_variables(context)
|
|
327
|
+
stream&.call(OmniAgent::Streaming::Event.done(response))
|
|
302
328
|
return response
|
|
303
329
|
end
|
|
304
330
|
end
|
|
305
331
|
end
|
|
306
332
|
|
|
307
333
|
def available_tools
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
namespace_tools = if tool_namespace
|
|
311
|
-
tool_namespace.constants.filter_map do |const_name|
|
|
312
|
-
const = tool_namespace.const_get(const_name)
|
|
313
|
-
const if const.is_a?(Class) && const < OmniAgent::Tool
|
|
314
|
-
end
|
|
315
|
-
else
|
|
316
|
-
[]
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
namespace_tools + self.class.configured_delegated_tool_classes
|
|
334
|
+
self.class.tool_classes
|
|
320
335
|
end
|
|
321
336
|
|
|
322
337
|
private
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
module OmniAgent
|
|
2
2
|
class Configuration
|
|
3
3
|
attr_accessor :default_provider, :default_model, :max_retries, :retry_base_delay, :max_tool_iterations,
|
|
4
|
-
:max_delegation_depth, :eval_judge_provider, :eval_judge_model, :eval_cache_enabled, :eval_cache_path
|
|
4
|
+
:max_delegation_depth, :eval_judge_provider, :eval_judge_model, :eval_cache_enabled, :eval_cache_path,
|
|
5
|
+
:mcp_allowed_origins, :mcp_max_request_bytes
|
|
5
6
|
|
|
6
7
|
def initialize
|
|
7
8
|
@default_provider = :openai
|
|
@@ -14,6 +15,8 @@ module OmniAgent
|
|
|
14
15
|
@eval_judge_model = nil
|
|
15
16
|
@eval_cache_enabled = true
|
|
16
17
|
@eval_cache_path = "tmp/omni_agent_eval_cache.json"
|
|
18
|
+
@mcp_allowed_origins = []
|
|
19
|
+
@mcp_max_request_bytes = 1_048_576
|
|
17
20
|
end
|
|
18
21
|
end
|
|
19
22
|
end
|
data/lib/omni_agent/engine.rb
CHANGED
data/lib/omni_agent/errors.rb
CHANGED
|
@@ -6,6 +6,7 @@ module OmniAgent
|
|
|
6
6
|
class MaxToolIterationsError < Error; end
|
|
7
7
|
class MaxDelegationDepthError < Error; end
|
|
8
8
|
class EvalAssertionError < Error; end
|
|
9
|
+
class MCPError < Error; end
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
Error = Errors::Error
|
|
@@ -14,4 +15,5 @@ module OmniAgent
|
|
|
14
15
|
MaxToolIterationsError = Errors::MaxToolIterationsError
|
|
15
16
|
MaxDelegationDepthError = Errors::MaxDelegationDepthError
|
|
16
17
|
EvalAssertionError = Errors::EvalAssertionError
|
|
18
|
+
MCPError = Errors::MCPError
|
|
17
19
|
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require "active_support/security_utils"
|
|
2
|
+
|
|
3
|
+
module OmniAgent
|
|
4
|
+
module MCP
|
|
5
|
+
class Authenticator
|
|
6
|
+
Result = Struct.new(:authenticated, :principal) do
|
|
7
|
+
def authenticated?
|
|
8
|
+
authenticated == true
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
ARRAY_TOKEN_PRINCIPAL = :bearer_token
|
|
13
|
+
|
|
14
|
+
def initialize(server_class)
|
|
15
|
+
@server_class = server_class
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def authenticate(request)
|
|
19
|
+
authentication = @server_class.configured_authentication
|
|
20
|
+
|
|
21
|
+
if authentication.nil?
|
|
22
|
+
raise OmniAgent::MCPError,
|
|
23
|
+
"#{@server_class.name} does not declare authentication. " \
|
|
24
|
+
"Add `authenticate :bearer, tokens: ...`, `authenticate { |request| ... }`, " \
|
|
25
|
+
"or explicitly opt out with `authenticate :none`."
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
case authentication[:strategy]
|
|
29
|
+
when :none
|
|
30
|
+
success(nil)
|
|
31
|
+
when :bearer
|
|
32
|
+
authenticate_bearer(request, authentication[:tokens])
|
|
33
|
+
when :custom
|
|
34
|
+
principal = authentication[:block].call(request)
|
|
35
|
+
principal ? success(principal) : failure
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def authenticate_bearer(request, tokens)
|
|
42
|
+
presented = request.bearer_token
|
|
43
|
+
return failure if presented.nil?
|
|
44
|
+
|
|
45
|
+
matched = false
|
|
46
|
+
principal = nil
|
|
47
|
+
|
|
48
|
+
# Compare against every configured token (no early exit) so response
|
|
49
|
+
# timing does not reveal which token, or how many tokens, matched.
|
|
50
|
+
token_candidates(tokens).each do |candidate_principal, token|
|
|
51
|
+
next if token.nil? || token.to_s.empty?
|
|
52
|
+
|
|
53
|
+
if ActiveSupport::SecurityUtils.secure_compare(token.to_s, presented) && !matched
|
|
54
|
+
matched = true
|
|
55
|
+
principal = candidate_principal
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
matched ? success(principal) : failure
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def token_candidates(tokens)
|
|
63
|
+
tokens = tokens.call if tokens.respond_to?(:call)
|
|
64
|
+
|
|
65
|
+
case tokens
|
|
66
|
+
when Hash
|
|
67
|
+
tokens.map { |principal, token| [ principal, token ] }
|
|
68
|
+
else
|
|
69
|
+
Array(tokens).map { |token| [ ARRAY_TOKEN_PRINCIPAL, token ] }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def success(principal)
|
|
74
|
+
Result.new(true, principal)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def failure
|
|
78
|
+
Result.new(false, nil)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|