omni_agent 0.1.10 → 0.2.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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/README.md +100 -2
  4. data/Rakefile +4 -0
  5. data/app/jobs/omni_agent/embed_job.rb +10 -0
  6. data/app/jobs/omni_agent/ingest_job.rb +12 -0
  7. data/docs/agent/callbacks.md +60 -0
  8. data/docs/agent/delegation.md +56 -0
  9. data/docs/agent/provider.md +93 -0
  10. data/docs/agent/run-entrypoints.md +81 -0
  11. data/docs/agent/streaming.md +62 -0
  12. data/docs/agent/structured-output.md +80 -0
  13. data/docs/configuration.md +68 -0
  14. data/docs/evals/caching.md +21 -0
  15. data/docs/evals/generating-and-running.md +87 -0
  16. data/docs/evals/golden-sets.md +32 -0
  17. data/docs/evals/judge-provider.md +20 -0
  18. data/docs/generators.md +143 -0
  19. data/docs/index.md +98 -0
  20. data/docs/intro.md +93 -0
  21. data/docs/mcp/authentication.md +226 -0
  22. data/docs/mcp/defining-servers.md +196 -0
  23. data/docs/mcp/transports.md +166 -0
  24. data/docs/rag.md +372 -0
  25. data/docs/stock-agents/ocr.md +52 -0
  26. data/docs/stock-agents/overview.md +91 -0
  27. data/docs/supported-providers.md +47 -0
  28. data/docs/tools.md +171 -0
  29. data/exe/omni_agent +48 -10
  30. data/lib/generators/omni_agent/chunks/chunks_generator.rb +130 -0
  31. data/lib/generators/omni_agent/embeddings/embeddings_generator.rb +72 -0
  32. data/lib/generators/omni_agent/install/install_generator.rb +78 -1
  33. data/lib/generators/omni_agent/mcp_server/mcp_server_generator.rb +4 -0
  34. data/lib/generators/omni_agent/stock_agent/stock_agent_generator.rb +73 -0
  35. data/lib/generators/omni_agent/vector_store.rb +113 -0
  36. data/lib/omni_agent/agent.rb +168 -39
  37. data/lib/omni_agent/chunkable.rb +181 -0
  38. data/lib/omni_agent/chunkers/hierarchical.rb +20 -0
  39. data/lib/omni_agent/chunkers/markdown.rb +41 -0
  40. data/lib/omni_agent/chunkers/recursive.rb +53 -0
  41. data/lib/omni_agent/chunkers.rb +31 -0
  42. data/lib/omni_agent/configuration.rb +14 -1
  43. data/lib/omni_agent/docs.rb +32 -0
  44. data/lib/omni_agent/embeddable.rb +131 -0
  45. data/lib/omni_agent/engine.rb +11 -0
  46. data/lib/omni_agent/errors.rb +15 -0
  47. data/lib/omni_agent/loaders/html.rb +21 -0
  48. data/lib/omni_agent/loaders/markdown.rb +18 -0
  49. data/lib/omni_agent/loaders/pdf.rb +25 -0
  50. data/lib/omni_agent/loaders/text.rb +15 -0
  51. data/lib/omni_agent/loaders.rb +68 -0
  52. data/lib/omni_agent/mcp/authenticator.rb +5 -2
  53. data/lib/omni_agent/mcp/oauth/access_token.rb +60 -0
  54. data/lib/omni_agent/mcp/oauth/http_client.rb +52 -0
  55. data/lib/omni_agent/mcp/oauth/introspection_verifier.rb +43 -0
  56. data/lib/omni_agent/mcp/oauth/jwt_verifier.rb +122 -0
  57. data/lib/omni_agent/mcp/oauth/resource_server.rb +166 -0
  58. data/lib/omni_agent/mcp/oauth.rb +18 -0
  59. data/lib/omni_agent/mcp/rack_app.rb +31 -7
  60. data/lib/omni_agent/mcp/server.rb +22 -5
  61. data/lib/omni_agent/providers/base.rb +14 -0
  62. data/lib/omni_agent/providers/mock.rb +89 -4
  63. data/lib/omni_agent/providers/ollama.rb +4 -0
  64. data/lib/omni_agent/providers/openai.rb +33 -2
  65. data/lib/omni_agent/providers/response.rb +6 -1
  66. data/lib/omni_agent/providers.rb +7 -0
  67. data/lib/omni_agent/schema.rb +200 -0
  68. data/lib/omni_agent/semantic_search.rb +45 -0
  69. data/lib/omni_agent/stock_agent.rb +59 -0
  70. data/lib/omni_agent/stock_agents/ocr/prompt.md.erb +6 -0
  71. data/lib/omni_agent/stock_agents/ocr.rb +70 -0
  72. data/lib/omni_agent/stock_agents.rb +26 -0
  73. data/lib/omni_agent/tool.rb +7 -107
  74. data/lib/omni_agent/version.rb +1 -1
  75. data/lib/omni_agent.rb +15 -1
  76. data/lib/tasks/omni_agent_chunks_tasks.rake +16 -0
  77. data/lib/tasks/omni_agent_embeddings_tasks.rake +15 -0
  78. metadata +55 -8
  79. data/app/assets/stylesheets/omni_agent/application.css +0 -15
  80. data/app/controllers/omni_agent/application_controller.rb +0 -4
  81. data/app/helpers/omni_agent/application_helper.rb +0 -4
  82. data/app/jobs/omni_agent/application_job.rb +0 -4
  83. data/app/mailers/omni_agent/application_mailer.rb +0 -6
  84. data/app/models/omni_agent/application_record.rb +0 -5
  85. data/app/views/layouts/omni_agent/application.html.erb +0 -17
data/docs/tools.md ADDED
@@ -0,0 +1,171 @@
1
+ <!-- Generated from omniagent-docs/docs/tools.mdx by `bundle exec rake docs:sync`. Do not edit by hand. -->
2
+ <!-- Online version: https://omniagent.activetaskers.com/docs/tools -->
3
+
4
+ # Tools
5
+
6
+ Tools are how agents take action beyond generating text. Every tool is a small class that declares an input schema and implements `#execute`.
7
+
8
+ ## Defining a Tool
9
+
10
+ Tools generated for an agent live under `app/agents/<agent_name>/tools/` and are namespaced under `<AgentClass>::Tools`:
11
+
12
+ ```ruby
13
+ module ResearchAgent::Tools
14
+ class GetWeather < OmniAgent::Tool
15
+ description "Get current weather for a city"
16
+ tags :weather
17
+ metadata category: :utility
18
+
19
+ input do
20
+ string :city, description: "City name"
21
+ end
22
+
23
+ def execute(city:)
24
+ "Sunny in #{city}"
25
+ end
26
+ end
27
+ end
28
+ ```
29
+
30
+ * **`description`**: Sent to the provider so the model knows when to call the tool.
31
+ * **`tags`**: Symbols used for filtering tools available to an agent.
32
+ * **`metadata`**: Free-form hash for your own bookkeeping (not sent to the provider).
33
+ * **`input`**: Declares the JSON-schema-style parameters the model must supply.
34
+ * **`execute`**: Runs when the model calls the tool. Receives the parsed arguments as keyword args.
35
+
36
+ ## MCP Title and Annotations
37
+
38
+ When a tool is exposed through an [MCP server](mcp/defining-servers.md), you can add a display title and behavior hints for MCP clients:
39
+
40
+ ```ruby
41
+ class GetWeather < OmniAgent::Tool
42
+ title "Get weather"
43
+ annotations read_only: true, destructive: false, idempotent: true, open_world: true
44
+ end
45
+ ```
46
+
47
+ These are sent only to MCP clients, not to LLM providers. See [Tool Annotations](mcp/defining-servers.md#tool-annotations).
48
+
49
+ ## Input Schema DSL
50
+
51
+ Use `input do ... end` to declare typed parameters:
52
+
53
+ ```ruby
54
+ input do
55
+ string :city, description: "City name"
56
+ integer :days, description: "Forecast length", required: false
57
+ number :confidence, min: 0.0, max: 1.0, required: false
58
+ boolean :metric, required: false
59
+ array :tags, items_type: "string"
60
+ hash :coordinates do
61
+ string :lat
62
+ string :lng
63
+ end
64
+ enum :unit, values: [ "celsius", "fahrenheit" ], description: "Temperature unit"
65
+ end
66
+ ```
67
+
68
+ Supported field types:
69
+
70
+ * **`string`**
71
+ * **`integer`**
72
+ * **`number`** — floating-point values
73
+ * **`boolean`**
74
+ * **`array`** — pass `items_type:` for primitive items, or a block for an array of objects
75
+ * **`hash`** — pass a block to declare nested properties, or omit it for a free-form object
76
+ * **`enum`** — pass `values:` as a non-empty array of same-typed values (all strings, all integers, all floats, or all booleans). Mixed-type or empty value sets raise `ArgumentError` at schema-definition time.
77
+ * **`polymorphic`** — see [Polymorphic References](#polymorphic-references) below.
78
+
79
+ All fields are `required: true` by default. Pass `required: false` to make a field optional.
80
+
81
+ `invoke` validates arguments against the schema before calling `#execute`: a missing required field, an enum value outside `values:`, a constraint violation, or a failing custom validator all raise `ArgumentError` rather than reaching your tool code.
82
+
83
+ ## Validations
84
+
85
+ `string`, `integer`, `number`, and `array` accept optional constraint kwargs. These are emitted as standard JSON Schema keywords (so the model sees them too) and enforced again at runtime by `invoke`:
86
+
87
+ ```ruby
88
+ input do
89
+ string :name, min_length: 3, max_length: 40, pattern: /\A[a-z]+\z/i, format: "email"
90
+ integer :level, min: 1, max: 5
91
+ number :ratio, min: 0.0, max: 1.0
92
+ array :tags, items_type: "string", min_items: 1, max_items: 10
93
+ end
94
+ ```
95
+
96
+ | Kwarg | Applies to | JSON Schema keyword |
97
+ |---|---|---|
98
+ | `min_length:` / `max_length:` | `string` | `minLength` / `maxLength` |
99
+ | `pattern:` (`Regexp` or `String`) | `string` | `pattern` |
100
+ | `format:` | `string` | `format` |
101
+ | `min:` / `max:` | `integer`, `number` | `minimum` / `maximum` |
102
+ | `min_items:` / `max_items:` | `array` | `minItems` / `maxItems` |
103
+
104
+ Every field type also accepts `validate:`, a `Proc` for arbitrary runtime rules. It is never serialized into the schema sent to the provider — it only runs locally in `invoke`. Return `false` to raise a generic `ArgumentError`, or raise your own `ArgumentError` for a custom message:
105
+
106
+ ```ruby
107
+ input do
108
+ string :slug, validate: ->(v) { v == v.downcase or raise ArgumentError, "slug must be lowercase" }
109
+ end
110
+ ```
111
+
112
+ ## Polymorphic References
113
+
114
+ `polymorphic :name` declares a Rails-style polymorphic reference. It expands into two ordinary schema fields, `name_type` and `name_id`:
115
+
116
+ ```ruby
117
+ input do
118
+ polymorphic :actor,
119
+ types: [ "User", "Admin" ], # allowed actor_type values -> becomes an enum
120
+ description: "The actor performing the action",
121
+ id_type: :integer, # actor_id schema type, default "string"
122
+ resolve: true # fetch the actual record instead of raw type/id
123
+ end
124
+ ```
125
+
126
+ Individual descriptions for each sub-field can be set with the block form instead of a shared `description:`:
127
+
128
+ ```ruby
129
+ polymorphic :actor, types: [ "User", "Admin" ], resolve: true do
130
+ type values: [ "User", "Admin" ], description: "The actor's class name"
131
+ id type: :integer, description: "The actor's primary key"
132
+ end
133
+ ```
134
+
135
+ Two runtime modes:
136
+
137
+ * **`resolve: false` (default)** — `#execute` receives `actor_type:` and `actor_id:` as separate keyword arguments. `actor_type` is validated against `types:` like any other enum.
138
+ * **`resolve: true`** — requires `types:`. At runtime, `invoke` looks up the class named by `actor_type` (restricted to the `types:` whitelist — never an arbitrary string) via `Object.const_get`, calls `.find(actor_id)` on it, and passes the result as a single `actor:` keyword argument instead. A record that isn't found raises `ArgumentError`, which the agent loop surfaces back to the model as a retryable tool error, just like an invalid enum value.
139
+
140
+ `resolve: true` without `types:` raises `ArgumentError` at schema-definition time.
141
+
142
+ ## Stopping Generation Early
143
+
144
+ Sometimes a tool call should end the agent's run instead of looping back to the model — for example, a tool that hands off to a human or returns a final answer directly.
145
+
146
+ Declare it at the class level:
147
+
148
+ ```ruby
149
+ class Escalate < OmniAgent::Tool
150
+ stops_generation
151
+
152
+ def execute(reason:)
153
+ "Escalated: #{reason}"
154
+ end
155
+ end
156
+ ```
157
+
158
+ Or trigger it conditionally from inside `#execute`:
159
+
160
+ ```ruby
161
+ def execute(reason:)
162
+ stop_generation! if reason == "urgent"
163
+ "Logged: #{reason}"
164
+ end
165
+ ```
166
+
167
+ ## How Tools Are Discovered
168
+
169
+ `OmniAgent::Agent#available_tools` looks up the `<AgentClass>::Tools` namespace and collects every constant that is a subclass of `OmniAgent::Tool`. There's no manual registration step — defining a tool class in the right namespace is enough for the agent to pick it up.
170
+
171
+ The class method `AgentClass.tool_classes` returns the same list without creating an agent instance (and therefore without a provider). [`tools_from`](mcp/defining-servers.md) in MCP servers uses it.
data/exe/omni_agent CHANGED
@@ -3,13 +3,24 @@
3
3
 
4
4
  require "optparse"
5
5
 
6
- options = { fresh: false }
7
- parser = OptionParser.new do |opts|
8
- opts.banner = <<~BANNER
9
- Usage: omni_agent eval [pattern] [options]
10
- omni_agent mcp <ServerClass>
11
- BANNER
12
- opts.on("--fresh", "Clear the eval cache before running") { options[:fresh] = true }
6
+ BANNER = <<~BANNER
7
+ Usage: omni_agent eval [pattern] [--fresh]
8
+ omni_agent mcp <ServerClass>
9
+ omni_agent docs [page] [--path]
10
+ BANNER
11
+
12
+ # Each command gets its own parser, so a flag meant for another command is
13
+ # rejected instead of silently ignored.
14
+ def parse_options(options)
15
+ OptionParser.new do |opts|
16
+ opts.banner = BANNER
17
+ yield opts, options
18
+ end.parse!(ARGV)
19
+ options
20
+ rescue OptionParser::ParseError => e
21
+ warn e.message
22
+ warn BANNER
23
+ exit 1
13
24
  end
14
25
 
15
26
  def require_rails_environment(command)
@@ -24,7 +35,9 @@ command = ARGV.shift
24
35
 
25
36
  case command
26
37
  when "eval"
27
- parser.parse!(ARGV)
38
+ options = parse_options(fresh: false) do |opts, parsed|
39
+ opts.on("--fresh", "Clear the eval cache before running") { parsed[:fresh] = true }
40
+ end
28
41
  pattern = ARGV.shift
29
42
 
30
43
  require_rails_environment("eval")
@@ -48,11 +61,36 @@ when "mcp"
48
61
 
49
62
  require_rails_environment("mcp")
50
63
  OmniAgent::MCP::Stdio.start(server)
64
+ when "docs"
65
+ options = parse_options(path: false) do |opts, parsed|
66
+ opts.on("--path", "Print the path of the bundled docs instead of their contents") { parsed[:path] = true }
67
+ end
68
+ require_relative "../lib/omni_agent/docs"
69
+
70
+ # docs/ is generated when the gem is built, so a gem installed from git
71
+ # (`gem "omni_agent", github: ...`) doesn't have it.
72
+ unless File.directory?(OmniAgent::Docs.root)
73
+ abort "The bundled docs are missing (they're generated by `rake build`). Read them online at https://omniagent.activetaskers.com/docs/intro"
74
+ end
75
+
76
+ page = ARGV.shift
77
+ file = OmniAgent::Docs.path(page || "index")
78
+ unless file
79
+ warn "Unknown docs page: #{page.inspect}. Available pages:"
80
+ warn OmniAgent::Docs.pages.map { |name| " #{name}" }
81
+ exit 1
82
+ end
83
+
84
+ if options[:path]
85
+ puts page ? file : OmniAgent::Docs.root
86
+ else
87
+ puts File.read(file)
88
+ end
51
89
  when nil
52
- warn parser.banner
90
+ warn BANNER
53
91
  exit 1
54
92
  else
55
93
  warn "Unknown command: #{command.inspect}"
56
- warn parser.banner
94
+ warn BANNER
57
95
  exit 1
58
96
  end
@@ -0,0 +1,130 @@
1
+ require "rails/generators/named_base"
2
+ require_relative "../vector_store"
3
+
4
+ module OmniAgent
5
+ module Generators
6
+ class ChunksGenerator < Rails::Generators::NamedBase
7
+ include VectorStore
8
+
9
+ desc "Creates a chunk model for a document model and declares has_chunks on it"
10
+
11
+ argument :source, type: :string, banner: "source", desc: "Attachment, text column or rich text to chunk"
12
+
13
+ class_option :as, type: :string, desc: "Loader to enforce (pdf, html, markdown, text). Default: detected per file"
14
+ class_option :chunker, type: :string, enum: OmniAgent::Chunkers.registry.keys.map(&:to_s), desc: "Default: recursive"
15
+ class_option :size, type: :numeric, desc: "Chunk size in characters. Default: 1000"
16
+ class_option :overlap, type: :numeric, desc: "Characters repeated between chunks. Default: 200"
17
+ class_option :inline, type: :boolean, default: false, desc: "Ingest during save instead of in a background job"
18
+ class_option :dimensions, type: :numeric, desc: "Vector size (default: detected by calling the embedding model once)"
19
+ class_option :backend, type: :string, enum: BACKENDS, desc: "Default: detected from the database adapter"
20
+
21
+ def check_backend_and_dimensions
22
+ raise Thor::Error, "#{model_path} not found. Generate the model first." unless File.exist?(File.join(destination_root, model_path))
23
+
24
+ backend
25
+ dimensions
26
+ end
27
+
28
+ def create_migration_file
29
+ migration_file("Create#{chunk_table.camelize}", [
30
+ "create_table :#{chunk_table} do |t|",
31
+ " t.references :#{reference}, null: false, foreign_key: { to_table: :#{table_name} }",
32
+ " t.integer :position, null: false",
33
+ " t.text :content, null: false",
34
+ " t.text :context",
35
+ " t.json :metadata",
36
+ "end",
37
+ "add_index :#{chunk_table}, [ :#{reference}_id, :position ]",
38
+ *vector_column_lines(chunk_table, "embedding")
39
+ ])
40
+ end
41
+
42
+ def create_vector_store_initializer
43
+ create_sqlite_initializer
44
+ end
45
+
46
+ def create_chunk_model
47
+ lines = [ "class #{chunk_class} < ApplicationRecord" ]
48
+ lines << " belongs_to :#{reference}#{", class_name: \"#{class_name}\"" if reference.camelize != class_name}"
49
+ lines << " # Ruby backend, small tables only: every search loads and ranks all rows in Ruby." if backend == "ruby"
50
+ lines << " has_embedding #{[ ':content', *vector_store_options ].join(', ')}"
51
+ lines << "end"
52
+ create_file File.join("app", "models", "#{file_path}_chunk.rb"), lines.join("\n") + "\n"
53
+ end
54
+
55
+ def add_has_chunks
56
+ lines = []
57
+ lines << " has_one_attached :#{source}\n" if attachment_needed?
58
+ lines << " has_chunks #{chunk_arguments.join(', ')}\n"
59
+ inject_into_class model_path, class_name, lines.join
60
+ end
61
+
62
+ def show_next_steps
63
+ say ""
64
+ say_vector_store_setup
65
+ say "To read PDFs, also:"
66
+ say " bundle add pdf-reader"
67
+ say "Then migrate and ingest existing records:"
68
+ say " bin/rails active_storage:install # if #{source} is an upload and you haven't" if attachment?
69
+ say " bin/rails db:migrate"
70
+ say " bin/rails \"omni_agent:chunks:ingest[#{class_name}]\""
71
+ say ""
72
+ say "Give an agent a search tool with:"
73
+ say " searches #{class_name}"
74
+ end
75
+
76
+ private
77
+
78
+ def model_path
79
+ File.join("app", "models", "#{file_path}.rb")
80
+ end
81
+
82
+ def chunk_class
83
+ "#{class_name}Chunk"
84
+ end
85
+
86
+ def chunk_table
87
+ "#{singular_table_name}_chunks"
88
+ end
89
+
90
+ def reference
91
+ class_name.demodulize.underscore
92
+ end
93
+
94
+ def chunk_arguments
95
+ arguments = [ ":#{source}" ]
96
+ arguments << "as: :#{options[:as]}" if options[:as]
97
+ arguments << "chunker: :#{options[:chunker]}" if options[:chunker]
98
+ arguments << "size: #{options[:size]}" if options[:size]
99
+ arguments << "overlap: #{options[:overlap]}" if options[:overlap]
100
+ arguments << "later: false" if options[:inline]
101
+ arguments
102
+ end
103
+
104
+ def attachment?
105
+ attachment_needed? || model_source.match?(/has_(one|many)_attached\s+:#{source}\b/)
106
+ end
107
+
108
+ # A source the model doesn't declare and the table doesn't have is an upload.
109
+ def attachment_needed?
110
+ return @attachment_needed if defined?(@attachment_needed)
111
+
112
+ @attachment_needed = !model_source.match?(/(has_\w+|def|attribute|alias_attribute)\s+:?#{source}\b/) && !source_column?
113
+ end
114
+
115
+ def model_source
116
+ File.read(File.join(destination_root, model_path))
117
+ end
118
+
119
+ # In the database, or in a migration that may not have run yet.
120
+ def source_column?
121
+ migrations = Dir[File.join(destination_root, "db", "migrate", "*.rb")].map { |path| File.read(path) }
122
+ return true if migrations.any? { |migration| migration.include?(":#{table_name}") && migration.match?(/[:"]#{source}\b/) }
123
+
124
+ ActiveRecord::Base.with_connection { |connection| connection.table_exists?(table_name) && connection.column_exists?(table_name, source) }
125
+ rescue ActiveRecord::ActiveRecordError
126
+ false
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,72 @@
1
+ require "rails/generators/named_base"
2
+ require_relative "../vector_store"
3
+
4
+ module OmniAgent
5
+ module Generators
6
+ class EmbeddingsGenerator < Rails::Generators::NamedBase
7
+ include VectorStore
8
+
9
+ desc "Adds an embedding column to a model and declares has_embedding on it"
10
+
11
+ argument :sources, type: :array, banner: "attribute [attribute ...]", desc: "Attributes to embed"
12
+
13
+ class_option :column, type: :string, default: "embedding", desc: "Column that stores the vector"
14
+ class_option :dimensions, type: :numeric, desc: "Vector size (default: detected by calling the embedding model once)"
15
+ class_option :backend, type: :string, enum: BACKENDS, desc: "Default: detected from the database adapter"
16
+ class_option :later, type: :boolean, default: false, desc: "Embed in a background job after commit"
17
+
18
+ def check_backend_and_dimensions
19
+ backend
20
+ dimensions
21
+ end
22
+
23
+ def create_migration_file
24
+ migration_file(migration_name, vector_column_lines(table_name, column))
25
+ end
26
+
27
+ def create_vector_store_initializer
28
+ create_sqlite_initializer
29
+ end
30
+
31
+ def add_has_embedding
32
+ model_path = File.join("app", "models", "#{file_path}.rb")
33
+ raise Thor::Error, "#{model_path} not found. Generate the model first." unless File.exist?(File.join(destination_root, model_path))
34
+
35
+ inject_into_class model_path, class_name, model_lines
36
+ end
37
+
38
+ def show_next_steps
39
+ say ""
40
+ say_vector_store_setup
41
+ say "Then migrate and embed existing records:"
42
+ say " bin/rails db:migrate"
43
+ say " bin/rails \"omni_agent:embeddings:backfill[#{class_name}]\""
44
+ say ""
45
+ say "Give an agent a search tool with:"
46
+ say " searches #{class_name}"
47
+ end
48
+
49
+ private
50
+
51
+ def column
52
+ options[:column]
53
+ end
54
+
55
+ def migration_name
56
+ "Add#{column.camelize}To#{table_name.camelize}"
57
+ end
58
+
59
+ def model_lines
60
+ arguments = sources.map { |source| ":#{source}" }
61
+ arguments << "column: :#{column}" unless column == "embedding"
62
+ arguments.concat(vector_store_options)
63
+ arguments << "later: true" if options[:later]
64
+
65
+ lines = []
66
+ lines << " # Ruby backend, small tables only: every search loads and ranks all rows in Ruby.\n" if backend == "ruby"
67
+ lines << " has_embedding #{arguments.join(', ')}\n"
68
+ lines.join
69
+ end
70
+ end
71
+ end
72
+ end
@@ -4,7 +4,55 @@ require "fileutils"
4
4
  module OmniAgent
5
5
  module Generators
6
6
  class InstallGenerator < Rails::Generators::Base
7
- desc "Creates OmniAgent base directories in app/agents"
7
+ desc "Creates OmniAgent base directories in app/agents and config/initializers/omni_agent.rb"
8
+
9
+ INITIALIZER = <<~RUBY
10
+ OmniAgent.configure do |config|
11
+ # Every setting below shows its default. Uncomment to change it.
12
+
13
+ # Agents
14
+ # config.default_provider = :openai # :openai, :ollama, :mock
15
+ # config.default_model = "gpt-4o-mini"
16
+ # config.max_tool_iterations = 10
17
+ # config.max_delegation_depth = 5
18
+
19
+ # Provider retries (rate limits, timeouts, 5xx), with exponential backoff
20
+ # config.max_retries = 3
21
+ # config.retry_base_delay = 0.5
22
+
23
+ # Embeddings and RAG (has_embedding, searches)
24
+ # config.embedding_provider = nil # nil uses default_provider; use :mock in tests
25
+ # config.embedding_model = nil # nil uses the provider's default embedding model
26
+ # config.embedding_job_options = {} # for has_embedding later: true, e.g. { queue: :embeddings, priority: 10 }
27
+ # config.document_loaders = {} # extra has_chunks loaders, e.g. { docx: DocxLoader }
28
+
29
+ # Evals
30
+ # config.eval_judge_provider = nil # nil uses the evaluated agent's provider
31
+ # config.eval_judge_model = nil
32
+ # config.eval_cache_enabled = true
33
+ # config.eval_cache_path = "tmp/omni_agent_eval_cache.json"
34
+
35
+ # MCP servers
36
+ # config.mcp_allowed_origins = []
37
+ # config.mcp_max_request_bytes = 1_048_576
38
+ end
39
+ RUBY
40
+
41
+ class_option :skip_agents_md, type: :boolean, default: false,
42
+ desc: "Don't point AGENTS.md (and CLAUDE.md, if present) at the bundled OmniAgent docs"
43
+
44
+ AGENTS_MD_MARKER = "<!-- omni_agent:docs -->".freeze
45
+ AGENTS_MD_SECTION = <<~MARKDOWN.freeze
46
+ #{AGENTS_MD_MARKER}
47
+ ## OmniAgent
48
+
49
+ This app builds AI agents with the `omni_agent` gem. Its documentation ships
50
+ inside the gem: run `bundle exec omni_agent docs` for the index (conventions,
51
+ typical flows and every page), `bundle exec omni_agent docs <page>` to read a
52
+ page, or `bundle exec omni_agent docs --path` for the directory. Read it before
53
+ adding or changing agents (`app/agents/`), tools, MCP servers (`app/mcp_servers/`)
54
+ or evals (`evals/`), and prefer the `rails generate omni_agent:*` generators.
55
+ MARKDOWN
8
56
 
9
57
  def create_agents_root
10
58
  agents_root = File.join(destination_root, "app", "agents")
@@ -12,10 +60,39 @@ module OmniAgent
12
60
  say_status :create, agents_root
13
61
  end
14
62
 
63
+ def create_initializer
64
+ path = File.join("config", "initializers", "omni_agent.rb")
65
+ return say_status(:skip, path, :yellow) if File.exist?(File.join(destination_root, path))
66
+
67
+ create_file path, INITIALIZER
68
+ end
69
+
70
+ def point_coding_agents_at_docs
71
+ return if options[:skip_agents_md]
72
+
73
+ add_docs_section("AGENTS.md", create: true)
74
+ add_docs_section("CLAUDE.md", create: false)
75
+ end
76
+
15
77
  def show_next_steps
16
78
  say "OmniAgent install complete."
17
79
  say "Next: rails generate omni_agent:agent SupportAgent --with-tools Tool1 Tool2"
18
80
  say "Expose tools to MCP clients: rails generate omni_agent:mcp_server Support --agents SupportAgent"
81
+ say "Read the bundled docs: bundle exec omni_agent docs"
82
+ end
83
+
84
+ private
85
+
86
+ def add_docs_section(file, create:)
87
+ path = File.join(destination_root, file)
88
+
89
+ if File.exist?(path)
90
+ return say_status(:identical, file) if File.read(path).include?(AGENTS_MD_MARKER)
91
+
92
+ append_to_file file, "\n#{AGENTS_MD_SECTION}"
93
+ elsif create
94
+ create_file file, AGENTS_MD_SECTION
95
+ end
19
96
  end
20
97
  end
21
98
  end
@@ -101,6 +101,10 @@ module OmniAgent
101
101
 
102
102
  # Comma-separated list of accepted bearer tokens.
103
103
  authenticate :bearer, tokens: -> { ENV.fetch("#{token_env_var}", "").split(",") }
104
+ # Or OAuth 2.1 with tokens from your authorization server (add `gem "jwt"`):
105
+ # authenticate :oauth, resource: "https://app.example.com/mcp/...",
106
+ # authorization_servers: "https://auth.example.com",
107
+ # jwks_uri: "https://auth.example.com/.well-known/jwks.json"
104
108
 
105
109
  # Hide tools per principal:
106
110
  # authorize_tool { |tool_class, principal| true }
@@ -0,0 +1,73 @@
1
+ require "rails/generators/named_base"
2
+ require "fileutils"
3
+
4
+ module OmniAgent
5
+ module Generators
6
+ class StockAgentGenerator < Rails::Generators::NamedBase
7
+ FIELD_TYPES = %w[string integer number boolean].freeze
8
+
9
+ class_option :from,
10
+ type: :string,
11
+ required: true,
12
+ desc: "Stock agent to build on (e.g. ocr)"
13
+ class_option :fields,
14
+ type: :array,
15
+ default: [],
16
+ banner: "name:type name:type ...",
17
+ desc: "Output fields to declare (types: #{FIELD_TYPES.join(', ')})"
18
+
19
+ def create_agent_structure
20
+ agent_source = [
21
+ "class #{class_name} < #{stock_agent_class.name}",
22
+ " output do",
23
+ *output_field_lines.map { |line| " #{line}" },
24
+ " end",
25
+ "end"
26
+ ].join("\n")
27
+
28
+ FileUtils.mkdir_p(agent_directory)
29
+ create_file(agent_file_path, "#{agent_source}\n")
30
+
31
+ create_file(prompt_file_path, <<~ERB)
32
+ <%# Optional: extra instructions appended after the #{stock_agent_class.stock_name} stock agent prompt. %>
33
+ ERB
34
+ end
35
+
36
+ private
37
+
38
+ def stock_agent_class
39
+ @stock_agent_class ||= OmniAgent::StockAgents.find(options[:from])
40
+ rescue OmniAgent::UnknownStockAgentError => e
41
+ raise Thor::Error, e.message
42
+ end
43
+
44
+ def output_field_lines
45
+ fields = Array(options[:fields]).map(&:strip).reject(&:empty?)
46
+ return [ 'string :example_field, description: "Describe the value the agent must return"' ] if fields.empty?
47
+
48
+ fields.map do |field|
49
+ name, type = field.split(":", 2)
50
+ type = (type || "string").downcase
51
+
52
+ unless FIELD_TYPES.include?(type)
53
+ raise Thor::Error, "Unsupported field type #{type.inspect} for #{name}. Use one of: #{FIELD_TYPES.join(', ')}"
54
+ end
55
+
56
+ "#{type} :#{name.underscore}"
57
+ end
58
+ end
59
+
60
+ def agent_directory
61
+ File.join(destination_root, "app", "agents", file_name)
62
+ end
63
+
64
+ def agent_file_path
65
+ File.join(destination_root, "app", "agents", "#{file_name}.rb")
66
+ end
67
+
68
+ def prompt_file_path
69
+ File.join(agent_directory, "prompt.md.erb")
70
+ end
71
+ end
72
+ end
73
+ end