rails-agent-stack 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 +16 -0
- data/MIT-LICENSE +21 -0
- data/README.md +463 -0
- data/lib/generators/rails_agents/install_generator.rb +29 -0
- data/lib/generators/rails_agents/templates/initializer.rb +6 -0
- data/lib/rails/agent/stack.rb +4 -0
- data/lib/rails_agents/agent.rb +128 -0
- data/lib/rails_agents/configuration.rb +35 -0
- data/lib/rails_agents/error.rb +7 -0
- data/lib/rails_agents/generated_file.rb +15 -0
- data/lib/rails_agents/message.rb +27 -0
- data/lib/rails_agents/providers/anthropic/files.rb +51 -0
- data/lib/rails_agents/providers/anthropic.rb +111 -0
- data/lib/rails_agents/providers/base.rb +23 -0
- data/lib/rails_agents/providers/grok.rb +15 -0
- data/lib/rails_agents/providers/open_router.rb +19 -0
- data/lib/rails_agents/providers/openai.rb +15 -0
- data/lib/rails_agents/providers/openai_compatible.rb +78 -0
- data/lib/rails_agents/railtie.rb +25 -0
- data/lib/rails_agents/result.rb +13 -0
- data/lib/rails_agents/runner.rb +146 -0
- data/lib/rails_agents/skill.rb +24 -0
- data/lib/rails_agents/skill_declaration.rb +8 -0
- data/lib/rails_agents/skill_set.rb +139 -0
- data/lib/rails_agents/skills/anthropic_content.rb +29 -0
- data/lib/rails_agents/skills/portable/web_fetch.rb +29 -0
- data/lib/rails_agents/skills/portable/web_search.rb +33 -0
- data/lib/rails_agents/skills/registry.rb +67 -0
- data/lib/rails_agents/tool.rb +48 -0
- data/lib/rails_agents/tool_set.rb +46 -0
- data/lib/rails_agents/version.rb +5 -0
- data/lib/rails_agents.rb +44 -0
- data/rails-agent-stack.gemspec +49 -0
- metadata +150 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAgents
|
|
4
|
+
class SkillSet
|
|
5
|
+
FILES_BETA = "files-api-2025-04-14"
|
|
6
|
+
|
|
7
|
+
attr_reader :provider, :entries, :custom_skills, :declarations
|
|
8
|
+
|
|
9
|
+
def initialize(declarations:, provider:)
|
|
10
|
+
@provider = provider.to_sym
|
|
11
|
+
@declarations = declarations
|
|
12
|
+
@custom_skills = []
|
|
13
|
+
@options_by_name = declarations.to_h { |declaration|
|
|
14
|
+
key = declaration.custom? ? declaration.key.to_s : declaration.name.to_s
|
|
15
|
+
[key, declaration.options]
|
|
16
|
+
}
|
|
17
|
+
@entries = expand(resolve)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def server_tool_names
|
|
21
|
+
entries.select(&:server?).filter_map { |entry| entry.anthropic_tool&.dig(:name) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def portable_tool_classes
|
|
25
|
+
return [] unless use_portable?
|
|
26
|
+
|
|
27
|
+
entries.filter_map(&:portable_tool)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def anthropic_document_skills?
|
|
31
|
+
entries.any?(&:anthropic_document?) || custom_skills.any?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def anthropic_request
|
|
35
|
+
return {} unless provider == :anthropic
|
|
36
|
+
|
|
37
|
+
tools = entries.filter_map { |entry| anthropic_tool_for(entry) }
|
|
38
|
+
skills = entries.filter_map { |entry| anthropic_skill_for(entry) } + custom_skills
|
|
39
|
+
headers = anthropic_beta_headers(tools:, skills:)
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
server_tools: tools.uniq { |tool| tool[:name] },
|
|
43
|
+
container: (skills.empty? ? nil : {skills: skills}),
|
|
44
|
+
beta_headers: headers
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def validate!
|
|
49
|
+
if custom_skills.any? && provider != :anthropic
|
|
50
|
+
raise ConfigurationError, "Custom Anthropic skills require provider :anthropic"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
declarations.each do |declaration|
|
|
54
|
+
next if declaration.custom?
|
|
55
|
+
next if Skills::Registry.fetch(declaration.name)
|
|
56
|
+
|
|
57
|
+
raise ConfigurationError, "Unknown skill: #{declaration.key}. See RailsAgents::Skills::Registry::ENTRIES"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
entries.each do |entry|
|
|
61
|
+
next if provider == :anthropic
|
|
62
|
+
next if entry.portable_tool
|
|
63
|
+
|
|
64
|
+
raise ConfigurationError,
|
|
65
|
+
"Skill :#{entry.name} requires provider :anthropic. " \
|
|
66
|
+
"Portable skills: #{Skills::Registry::PORTABLE_SKILL_IDS.join(', ')}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def use_portable?
|
|
73
|
+
provider != :anthropic
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def resolve
|
|
77
|
+
declarations.filter_map do |declaration|
|
|
78
|
+
if declaration.custom?
|
|
79
|
+
@custom_skills << anthropic_custom_skill(declaration)
|
|
80
|
+
next
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
Skills::Registry.fetch(declaration.name)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def expand(resolved)
|
|
88
|
+
expanded = resolved.dup
|
|
89
|
+
resolved.each do |entry|
|
|
90
|
+
entry.requires.each do |required|
|
|
91
|
+
dependency = Skills::Registry.fetch(required)
|
|
92
|
+
expanded << dependency unless expanded.any? { |existing| existing.name == dependency.name }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
expanded.uniq { |entry| entry.name }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def anthropic_tool_for(entry)
|
|
99
|
+
return unless entry.anthropic_tool
|
|
100
|
+
|
|
101
|
+
tool = entry.anthropic_tool.dup
|
|
102
|
+
options = @options_by_name.fetch(entry.name.to_s, {})
|
|
103
|
+
merge_tool_options!(tool, options)
|
|
104
|
+
tool
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def anthropic_skill_for(entry)
|
|
108
|
+
return unless entry.anthropic_skill
|
|
109
|
+
|
|
110
|
+
skill = entry.anthropic_skill.dup
|
|
111
|
+
options = @options_by_name.fetch(entry.name.to_s, {})
|
|
112
|
+
skill[:version] = options.fetch(:version, skill[:version] || "latest")
|
|
113
|
+
skill
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def anthropic_custom_skill(declaration)
|
|
117
|
+
{
|
|
118
|
+
type: "custom",
|
|
119
|
+
skill_id: declaration.key.to_s,
|
|
120
|
+
version: declaration.options.fetch(:version, "latest")
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def merge_tool_options!(tool, options)
|
|
125
|
+
allowed = %i[max_uses allowed_domains blocked_domains user_location response_inclusion]
|
|
126
|
+
options.each do |key, value|
|
|
127
|
+
tool[key] = value if allowed.include?(key.to_sym)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def anthropic_beta_headers(tools:, skills:)
|
|
132
|
+
headers = []
|
|
133
|
+
headers << "code-execution-2025-08-25" if skills.any? || tools.any? { |tool| tool[:name] == "code_execution" }
|
|
134
|
+
headers << "skills-2025-10-02" if skills.any?
|
|
135
|
+
headers << FILES_BETA if skills.any? || tools.any? { |tool| tool[:name] == "code_execution" }
|
|
136
|
+
headers
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAgents
|
|
4
|
+
module Skills
|
|
5
|
+
module AnthropicContent
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def extract_text(blocks)
|
|
9
|
+
Array(blocks).select { |block| block["type"] == "text" }.map { |block| block["text"] }.join("\n").strip
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def extract_file_ids(blocks)
|
|
13
|
+
ids = []
|
|
14
|
+
walk(blocks) { |node| ids << node["file_id"] if node.is_a?(Hash) && !node["file_id"].to_s.empty? }
|
|
15
|
+
ids.uniq
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def walk(object)
|
|
19
|
+
case object
|
|
20
|
+
when Array
|
|
21
|
+
object.each { |item| walk(item) { |node| yield node } }
|
|
22
|
+
when Hash
|
|
23
|
+
yield object
|
|
24
|
+
object.each_value { |value| walk(value) { |node| yield node } }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
|
|
5
|
+
module RailsAgents
|
|
6
|
+
module Skills
|
|
7
|
+
module Portable
|
|
8
|
+
class WebFetch < Tool
|
|
9
|
+
description "Fetch and read the text content of a web page"
|
|
10
|
+
param :url, :string, description: "URL to fetch"
|
|
11
|
+
|
|
12
|
+
def call(url:)
|
|
13
|
+
response = Faraday.get(url, nil, {"User-Agent" => "RailsAgents/1.0"})
|
|
14
|
+
return {error: "Fetch failed", url: url, status: response.status} unless response.success?
|
|
15
|
+
|
|
16
|
+
text = response.body.gsub(/<script.*?>.*?<\/script>/m, "")
|
|
17
|
+
.gsub(/<style.*?>.*?<\/style>/m, "")
|
|
18
|
+
.gsub(/<[^>]+>/, " ")
|
|
19
|
+
.gsub(/\s+/, " ")
|
|
20
|
+
.strip
|
|
21
|
+
|
|
22
|
+
{url: url, content: text[0, 8_000]}
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
{error: e.message, url: url}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
|
|
5
|
+
module RailsAgents
|
|
6
|
+
module Skills
|
|
7
|
+
module Portable
|
|
8
|
+
class WebSearch < Tool
|
|
9
|
+
SEARCH_URL = "https://html.duckduckgo.com/html/"
|
|
10
|
+
|
|
11
|
+
description "Search the web for current information"
|
|
12
|
+
param :query, :string, description: "Search query"
|
|
13
|
+
|
|
14
|
+
def call(query:)
|
|
15
|
+
response = Faraday.get(SEARCH_URL, {q: query}, {"User-Agent" => "RailsAgents/1.0"})
|
|
16
|
+
return {error: "Search failed", query: query} unless response.success?
|
|
17
|
+
|
|
18
|
+
titles = response.body.scan(/class="result__a"[^>]*>(.*?)<\/a>/).first(5).map { |match|
|
|
19
|
+
match.first.gsub(/<[^>]+>/, "").strip
|
|
20
|
+
}.reject(&:empty?)
|
|
21
|
+
|
|
22
|
+
urls = response.body.scan(/class="result__url"[^>]*>(.*?)<\/a>/).first(5).map { |match|
|
|
23
|
+
match.first.gsub(/<[^>]+>/, "").strip
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
{query: query, results: titles.zip(urls).map { |title, url| {title:, url:} }}
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
{error: e.message, query: query}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAgents
|
|
4
|
+
module Skills
|
|
5
|
+
class Registry
|
|
6
|
+
ENTRIES = {
|
|
7
|
+
web_search: Skill.new(
|
|
8
|
+
name: :web_search,
|
|
9
|
+
kind: Skill::SERVER,
|
|
10
|
+
anthropic_tool: {type: "web_search_20260209", name: "web_search"},
|
|
11
|
+
portable_tool: Portable::WebSearch
|
|
12
|
+
),
|
|
13
|
+
web_fetch: Skill.new(
|
|
14
|
+
name: :web_fetch,
|
|
15
|
+
kind: Skill::SERVER,
|
|
16
|
+
anthropic_tool: {type: "web_fetch_20260309", name: "web_fetch"},
|
|
17
|
+
portable_tool: Portable::WebFetch
|
|
18
|
+
),
|
|
19
|
+
code_execution: Skill.new(
|
|
20
|
+
name: :code_execution,
|
|
21
|
+
kind: Skill::SERVER,
|
|
22
|
+
anthropic_tool: {type: "code_execution_20250825", name: "code_execution"}
|
|
23
|
+
),
|
|
24
|
+
memory: Skill.new(
|
|
25
|
+
name: :memory,
|
|
26
|
+
kind: Skill::SERVER,
|
|
27
|
+
anthropic_tool: {type: "memory_20250818", name: "memory"}
|
|
28
|
+
),
|
|
29
|
+
pptx: Skill.new(
|
|
30
|
+
name: :pptx,
|
|
31
|
+
kind: Skill::ANTHROPIC_DOCUMENT,
|
|
32
|
+
anthropic_skill: {type: "anthropic", skill_id: "pptx", version: "latest"},
|
|
33
|
+
requires: [:code_execution]
|
|
34
|
+
),
|
|
35
|
+
xlsx: Skill.new(
|
|
36
|
+
name: :xlsx,
|
|
37
|
+
kind: Skill::ANTHROPIC_DOCUMENT,
|
|
38
|
+
anthropic_skill: {type: "anthropic", skill_id: "xlsx", version: "latest"},
|
|
39
|
+
requires: [:code_execution]
|
|
40
|
+
),
|
|
41
|
+
docx: Skill.new(
|
|
42
|
+
name: :docx,
|
|
43
|
+
kind: Skill::ANTHROPIC_DOCUMENT,
|
|
44
|
+
anthropic_skill: {type: "anthropic", skill_id: "docx", version: "latest"},
|
|
45
|
+
requires: [:code_execution]
|
|
46
|
+
),
|
|
47
|
+
pdf: Skill.new(
|
|
48
|
+
name: :pdf,
|
|
49
|
+
kind: Skill::ANTHROPIC_DOCUMENT,
|
|
50
|
+
anthropic_skill: {type: "anthropic", skill_id: "pdf", version: "latest"},
|
|
51
|
+
requires: [:code_execution]
|
|
52
|
+
)
|
|
53
|
+
}.freeze
|
|
54
|
+
|
|
55
|
+
ANTHROPIC_SKILL_IDS = %i[pptx xlsx docx pdf].freeze
|
|
56
|
+
PORTABLE_SKILL_IDS = %i[web_search web_fetch].freeze
|
|
57
|
+
|
|
58
|
+
def self.fetch(name)
|
|
59
|
+
ENTRIES[name.to_sym]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.known?(name)
|
|
63
|
+
name.to_s.start_with?("skill_") || ENTRIES.key?(name.to_sym)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/string/inflections"
|
|
4
|
+
|
|
5
|
+
module RailsAgents
|
|
6
|
+
class Tool
|
|
7
|
+
class << self
|
|
8
|
+
attr_reader :description_text, :parameters
|
|
9
|
+
|
|
10
|
+
def inherited(subclass)
|
|
11
|
+
super
|
|
12
|
+
subclass.instance_variable_set(:@parameters, (parameters || {}).dup)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def description(text = nil)
|
|
16
|
+
return @description_text if text.nil?
|
|
17
|
+
@description_text = text
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def param(name, type, required: true, description: nil)
|
|
21
|
+
@parameters ||= {}
|
|
22
|
+
@parameters[name.to_sym] = {type: type.to_sym, required:, description:}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def tool_name
|
|
26
|
+
name.split("::").last.gsub(/Tool$/, "").underscore
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def definition
|
|
30
|
+
{
|
|
31
|
+
name: tool_name,
|
|
32
|
+
description: description_text,
|
|
33
|
+
parameters: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: parameters.transform_values { |p|
|
|
36
|
+
{type: p[:type].to_s, description: p[:description]}.compact
|
|
37
|
+
},
|
|
38
|
+
required: parameters.select { |_, p| p[:required] }.keys.map(&:to_s)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def call(**kwargs)
|
|
45
|
+
raise NotImplementedError
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAgents
|
|
4
|
+
class ToolSet
|
|
5
|
+
def initialize(*tools)
|
|
6
|
+
@tools = tools.flatten.compact.map { |t| resolve(t) }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.from_directory(path = nil)
|
|
10
|
+
path ||= (defined?(::Rails) && ::Rails.respond_to?(:root) ? ::Rails.root.join("app/agents/tools") : nil)
|
|
11
|
+
return new unless path && Dir.exist?(path)
|
|
12
|
+
|
|
13
|
+
classes = Dir.glob("#{path}/**/*.rb").filter_map do |file|
|
|
14
|
+
require_dependency file if defined?(Rails)
|
|
15
|
+
const_name = file.delete_prefix("#{path}/").delete_suffix(".rb").camelize
|
|
16
|
+
const_name.safe_constantize
|
|
17
|
+
end
|
|
18
|
+
new(*classes)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.use(*tools) = new(*tools)
|
|
22
|
+
|
|
23
|
+
def resolve(klass)
|
|
24
|
+
case klass
|
|
25
|
+
when Class
|
|
26
|
+
klass < Tool ? klass : klass
|
|
27
|
+
when String, Symbol
|
|
28
|
+
resolved = klass.to_s.camelize.constantize
|
|
29
|
+
raise Error, "#{klass} is not a RailsAgents::Tool" unless resolved < Tool
|
|
30
|
+
resolved
|
|
31
|
+
else
|
|
32
|
+
klass
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def definitions = @tools.map(&:definition)
|
|
37
|
+
def find(name) = @tools.find { |t| t.tool_name == name.to_s }
|
|
38
|
+
def execute(name, arguments)
|
|
39
|
+
tool_class = find(name)
|
|
40
|
+
raise Error, "Unknown tool: #{name}" unless tool_class
|
|
41
|
+
tool_class.new.call(**arguments.transform_keys(&:to_sym))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def +(other) = self.class.new(*@tools, *other.instance_variable_get(:@tools))
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/rails_agents.rb
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zeitwerk"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
loader = Zeitwerk::Loader.for_gem
|
|
7
|
+
loader.ignore("#{__dir__}/generators")
|
|
8
|
+
# Bundler auto-requires gem "rails-agent-stack" as rails/agent/stack —
|
|
9
|
+
# keep that shim off Zeitwerk so it does not define a stub Rails module.
|
|
10
|
+
loader.ignore("#{__dir__}/rails")
|
|
11
|
+
loader.inflector.inflect(
|
|
12
|
+
"openai" => "OpenAI",
|
|
13
|
+
"openai_compatible" => "OpenAICompatible",
|
|
14
|
+
"anthropic" => "Anthropic",
|
|
15
|
+
"open_router" => "OpenRouter"
|
|
16
|
+
)
|
|
17
|
+
loader.setup
|
|
18
|
+
|
|
19
|
+
require_relative "rails_agents/message"
|
|
20
|
+
require_relative "rails_agents/result"
|
|
21
|
+
require_relative "rails_agents/error"
|
|
22
|
+
require_relative "rails_agents/skill_declaration"
|
|
23
|
+
require_relative "rails_agents/generated_file"
|
|
24
|
+
require_relative "rails_agents/providers/openai_compatible"
|
|
25
|
+
|
|
26
|
+
module RailsAgents
|
|
27
|
+
class << self
|
|
28
|
+
attr_writer :config
|
|
29
|
+
|
|
30
|
+
def config
|
|
31
|
+
@config ||= Configuration.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def configure
|
|
35
|
+
yield config
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def tools(*classes)
|
|
39
|
+
ToolSet.use(*classes)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
require_relative "rails_agents/version"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/rails_agents/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "rails-agent-stack"
|
|
7
|
+
spec.version = RailsAgents::VERSION
|
|
8
|
+
spec.authors = ["Tiny Bubble Company"]
|
|
9
|
+
spec.email = ["hello@tinybubble.company"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Dead-simple AI agents for Rails — speed to production, not framework noise"
|
|
12
|
+
spec.description = <<~DESC.gsub(/\s+/, " ").strip
|
|
13
|
+
Rails Agents (gem: rails-agent-stack) is the simplest way to build AI agents
|
|
14
|
+
in Ruby on Rails. Define an LLM agent as a plain Ruby class, attach your app
|
|
15
|
+
code as tools, add skills like web search or spreadsheets, and call .run —
|
|
16
|
+
minutes to production, not days of framework setup.
|
|
17
|
+
|
|
18
|
+
Built for developers searching for Rails AI agents, Ruby LLM agents,
|
|
19
|
+
OpenAI / Anthropic / Claude / GPT tool-calling agents, OpenRouter and
|
|
20
|
+
Grok (xAI) integrations, agentic workflows, RAG helpers, and a lighter
|
|
21
|
+
alternative to RubyLLM, LangChain, or rolling your own multi-turn tool loop.
|
|
22
|
+
|
|
23
|
+
One mental model: RailsAgents::Agent. No dashboards, no cloud lock-in,
|
|
24
|
+
no agent lifecycle UI — just provider, model, description, tools, and skills.
|
|
25
|
+
DESC
|
|
26
|
+
|
|
27
|
+
spec.homepage = "https://tiny-bubble-company.github.io/rails-agents/"
|
|
28
|
+
spec.license = "MIT"
|
|
29
|
+
spec.required_ruby_version = ">= 3.2"
|
|
30
|
+
|
|
31
|
+
spec.metadata = {
|
|
32
|
+
"homepage_uri" => spec.homepage,
|
|
33
|
+
"source_code_uri" => "https://github.com/Tiny-Bubble-Company/rails-agents",
|
|
34
|
+
"changelog_uri" => "https://github.com/Tiny-Bubble-Company/rails-agents/blob/main/CHANGELOG.md",
|
|
35
|
+
"documentation_uri" => "https://tiny-bubble-company.github.io/rails-agents/",
|
|
36
|
+
"bug_tracker_uri" => "https://github.com/Tiny-Bubble-Company/rails-agents/issues",
|
|
37
|
+
"rubygems_mfa_required" => "true",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
spec.files = Dir.chdir(__dir__) do
|
|
41
|
+
Dir["{app,lib}/**/*", "README.md", "CHANGELOG.md", "MIT-LICENSE", "rails-agent-stack.gemspec"]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
spec.require_paths = ["lib"]
|
|
45
|
+
|
|
46
|
+
spec.add_dependency "faraday", ">= 2.9", "< 3"
|
|
47
|
+
spec.add_dependency "rails", ">= 7.1", "< 9"
|
|
48
|
+
spec.add_dependency "zeitwerk", ">= 2.6", "< 3"
|
|
49
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rails-agent-stack
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Tiny Bubble Company
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-09 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: faraday
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.9'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '3'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '2.9'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: rails
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.1'
|
|
40
|
+
- - "<"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '9'
|
|
43
|
+
type: :runtime
|
|
44
|
+
prerelease: false
|
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '7.1'
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '9'
|
|
53
|
+
- !ruby/object:Gem::Dependency
|
|
54
|
+
name: zeitwerk
|
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '2.6'
|
|
60
|
+
- - "<"
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '3'
|
|
63
|
+
type: :runtime
|
|
64
|
+
prerelease: false
|
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '2.6'
|
|
70
|
+
- - "<"
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '3'
|
|
73
|
+
description: 'Rails Agents (gem: rails-agent-stack) is the simplest way to build AI
|
|
74
|
+
agents in Ruby on Rails. Define an LLM agent as a plain Ruby class, attach your
|
|
75
|
+
app code as tools, add skills like web search or spreadsheets, and call .run — minutes
|
|
76
|
+
to production, not days of framework setup. Built for developers searching for Rails
|
|
77
|
+
AI agents, Ruby LLM agents, OpenAI / Anthropic / Claude / GPT tool-calling agents,
|
|
78
|
+
OpenRouter and Grok (xAI) integrations, agentic workflows, RAG helpers, and a lighter
|
|
79
|
+
alternative to RubyLLM, LangChain, or rolling your own multi-turn tool loop. One
|
|
80
|
+
mental model: RailsAgents::Agent. No dashboards, no cloud lock-in, no agent lifecycle
|
|
81
|
+
UI — just provider, model, description, tools, and skills.'
|
|
82
|
+
email:
|
|
83
|
+
- hello@tinybubble.company
|
|
84
|
+
executables: []
|
|
85
|
+
extensions: []
|
|
86
|
+
extra_rdoc_files: []
|
|
87
|
+
files:
|
|
88
|
+
- CHANGELOG.md
|
|
89
|
+
- MIT-LICENSE
|
|
90
|
+
- README.md
|
|
91
|
+
- lib/generators/rails_agents/install_generator.rb
|
|
92
|
+
- lib/generators/rails_agents/templates/initializer.rb
|
|
93
|
+
- lib/rails/agent/stack.rb
|
|
94
|
+
- lib/rails_agents.rb
|
|
95
|
+
- lib/rails_agents/agent.rb
|
|
96
|
+
- lib/rails_agents/configuration.rb
|
|
97
|
+
- lib/rails_agents/error.rb
|
|
98
|
+
- lib/rails_agents/generated_file.rb
|
|
99
|
+
- lib/rails_agents/message.rb
|
|
100
|
+
- lib/rails_agents/providers/anthropic.rb
|
|
101
|
+
- lib/rails_agents/providers/anthropic/files.rb
|
|
102
|
+
- lib/rails_agents/providers/base.rb
|
|
103
|
+
- lib/rails_agents/providers/grok.rb
|
|
104
|
+
- lib/rails_agents/providers/open_router.rb
|
|
105
|
+
- lib/rails_agents/providers/openai.rb
|
|
106
|
+
- lib/rails_agents/providers/openai_compatible.rb
|
|
107
|
+
- lib/rails_agents/railtie.rb
|
|
108
|
+
- lib/rails_agents/result.rb
|
|
109
|
+
- lib/rails_agents/runner.rb
|
|
110
|
+
- lib/rails_agents/skill.rb
|
|
111
|
+
- lib/rails_agents/skill_declaration.rb
|
|
112
|
+
- lib/rails_agents/skill_set.rb
|
|
113
|
+
- lib/rails_agents/skills/anthropic_content.rb
|
|
114
|
+
- lib/rails_agents/skills/portable/web_fetch.rb
|
|
115
|
+
- lib/rails_agents/skills/portable/web_search.rb
|
|
116
|
+
- lib/rails_agents/skills/registry.rb
|
|
117
|
+
- lib/rails_agents/tool.rb
|
|
118
|
+
- lib/rails_agents/tool_set.rb
|
|
119
|
+
- lib/rails_agents/version.rb
|
|
120
|
+
- rails-agent-stack.gemspec
|
|
121
|
+
homepage: https://tiny-bubble-company.github.io/rails-agents/
|
|
122
|
+
licenses:
|
|
123
|
+
- MIT
|
|
124
|
+
metadata:
|
|
125
|
+
homepage_uri: https://tiny-bubble-company.github.io/rails-agents/
|
|
126
|
+
source_code_uri: https://github.com/Tiny-Bubble-Company/rails-agents
|
|
127
|
+
changelog_uri: https://github.com/Tiny-Bubble-Company/rails-agents/blob/main/CHANGELOG.md
|
|
128
|
+
documentation_uri: https://tiny-bubble-company.github.io/rails-agents/
|
|
129
|
+
bug_tracker_uri: https://github.com/Tiny-Bubble-Company/rails-agents/issues
|
|
130
|
+
rubygems_mfa_required: 'true'
|
|
131
|
+
post_install_message:
|
|
132
|
+
rdoc_options: []
|
|
133
|
+
require_paths:
|
|
134
|
+
- lib
|
|
135
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
136
|
+
requirements:
|
|
137
|
+
- - ">="
|
|
138
|
+
- !ruby/object:Gem::Version
|
|
139
|
+
version: '3.2'
|
|
140
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - ">="
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '0'
|
|
145
|
+
requirements: []
|
|
146
|
+
rubygems_version: 3.4.1
|
|
147
|
+
signing_key:
|
|
148
|
+
specification_version: 4
|
|
149
|
+
summary: Dead-simple AI agents for Rails — speed to production, not framework noise
|
|
150
|
+
test_files: []
|