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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +16 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +463 -0
  5. data/lib/generators/rails_agents/install_generator.rb +29 -0
  6. data/lib/generators/rails_agents/templates/initializer.rb +6 -0
  7. data/lib/rails/agent/stack.rb +4 -0
  8. data/lib/rails_agents/agent.rb +128 -0
  9. data/lib/rails_agents/configuration.rb +35 -0
  10. data/lib/rails_agents/error.rb +7 -0
  11. data/lib/rails_agents/generated_file.rb +15 -0
  12. data/lib/rails_agents/message.rb +27 -0
  13. data/lib/rails_agents/providers/anthropic/files.rb +51 -0
  14. data/lib/rails_agents/providers/anthropic.rb +111 -0
  15. data/lib/rails_agents/providers/base.rb +23 -0
  16. data/lib/rails_agents/providers/grok.rb +15 -0
  17. data/lib/rails_agents/providers/open_router.rb +19 -0
  18. data/lib/rails_agents/providers/openai.rb +15 -0
  19. data/lib/rails_agents/providers/openai_compatible.rb +78 -0
  20. data/lib/rails_agents/railtie.rb +25 -0
  21. data/lib/rails_agents/result.rb +13 -0
  22. data/lib/rails_agents/runner.rb +146 -0
  23. data/lib/rails_agents/skill.rb +24 -0
  24. data/lib/rails_agents/skill_declaration.rb +8 -0
  25. data/lib/rails_agents/skill_set.rb +139 -0
  26. data/lib/rails_agents/skills/anthropic_content.rb +29 -0
  27. data/lib/rails_agents/skills/portable/web_fetch.rb +29 -0
  28. data/lib/rails_agents/skills/portable/web_search.rb +33 -0
  29. data/lib/rails_agents/skills/registry.rb +67 -0
  30. data/lib/rails_agents/tool.rb +48 -0
  31. data/lib/rails_agents/tool_set.rb +46 -0
  32. data/lib/rails_agents/version.rb +5 -0
  33. data/lib/rails_agents.rb +44 -0
  34. data/rails-agent-stack.gemspec +49 -0
  35. metadata +150 -0
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "pathname"
5
+
6
+ module RailsAgents
7
+ class Configuration
8
+ PROVIDERS = %i[openai anthropic openrouter grok].freeze
9
+
10
+ attr_accessor :openai_api_key, :anthropic_api_key, :openrouter_api_key, :grok_api_key,
11
+ :default_provider, :anthropic_auto_download_files, :anthropic_files_directory
12
+
13
+ def initialize
14
+ @openai_api_key = ENV["OPENAI_API_KEY"]
15
+ @anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
16
+ @openrouter_api_key = ENV["OPENROUTER_API_KEY"]
17
+ @grok_api_key = ENV["XAI_API_KEY"] || ENV["GROK_API_KEY"]
18
+ @default_provider = :openai
19
+ @anthropic_auto_download_files = true
20
+ @anthropic_files_directory = nil
21
+ end
22
+
23
+ def api_key_for(provider)
24
+ public_send(:"#{provider}_api_key")
25
+ end
26
+
27
+ def anthropic_files_directory!
28
+ return Pathname.new(@anthropic_files_directory) if @anthropic_files_directory.to_s != ""
29
+
30
+ return Rails.root.join("tmp/rails_agents/files") if defined?(Rails)
31
+
32
+ Pathname.new(Dir.mktmpdir("rails_agents_files_"))
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAgents
4
+ class Error < StandardError; end
5
+ class ConfigurationError < Error; end
6
+ class ProviderError < Error; end
7
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAgents
4
+ GeneratedFile = Data.define(:file_id, :filename, :content_type, :data, :path) do
5
+ def save(directory)
6
+ dir = File.expand_path(directory)
7
+ FileUtils.mkdir_p(dir)
8
+ destination = File.join(dir, filename)
9
+ File.binwrite(destination, data)
10
+ self.class.new(file_id:, filename:, content_type:, data:, path: destination)
11
+ end
12
+
13
+ def size = data.bytesize
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAgents
4
+ Message = Data.define(:role, :content, :tool_calls, :tool_call_id, :raw_content) do
5
+ def self.system(content) = new(role: :system, content:, tool_calls: nil, tool_call_id: nil, raw_content: nil)
6
+ def self.user(content) = new(role: :user, content:, tool_calls: nil, tool_call_id: nil, raw_content: nil)
7
+ def self.assistant(content, tool_calls: nil, raw_content: nil)
8
+ new(role: :assistant, content:, tool_calls:, tool_call_id: nil, raw_content:)
9
+ end
10
+ def self.tool(content, tool_call_id:) = new(role: :tool, content:, tool_calls: nil, tool_call_id:, raw_content: nil)
11
+ end
12
+
13
+ ToolCall = Data.define(:id, :name, :arguments)
14
+ Usage = Data.define(:input_tokens, :output_tokens) do
15
+ def total = input_tokens + output_tokens
16
+ end
17
+
18
+ ProviderResponse = Data.define(
19
+ :content,
20
+ :tool_calls,
21
+ :usage,
22
+ :finish_reason,
23
+ :content_blocks,
24
+ :file_ids,
25
+ :assistant_raw_content
26
+ )
27
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "faraday"
5
+
6
+ module RailsAgents
7
+ module Providers
8
+ class Anthropic
9
+ class Files
10
+ FILES_BETA = "files-api-2025-04-14"
11
+ API_VERSION = "2023-06-01"
12
+
13
+ def initialize(api_key: RailsAgents.config.anthropic_api_key)
14
+ raise ConfigurationError, "Set anthropic_api_key in config/initializers/rails_agents.rb" if api_key.to_s.empty?
15
+ @api_key = api_key
16
+ end
17
+
18
+ def download(file_id)
19
+ metadata = retrieve(file_id)
20
+ response = connection.get("files/#{file_id}/content")
21
+ raise ProviderError, "Failed to download file #{file_id}: #{response.body}" unless response.success?
22
+
23
+ GeneratedFile.new(
24
+ file_id: file_id,
25
+ filename: metadata.fetch("filename"),
26
+ content_type: metadata.fetch("mime_type"),
27
+ data: response.body,
28
+ path: nil
29
+ )
30
+ end
31
+
32
+ def retrieve(file_id)
33
+ response = connection.get("files/#{file_id}")
34
+ raise ProviderError, "Failed to retrieve file #{file_id}: #{response.body}" unless response.success?
35
+
36
+ JSON.parse(response.body)
37
+ end
38
+
39
+ private
40
+
41
+ def connection
42
+ @connection ||= Faraday.new(url: "https://api.anthropic.com/v1") do |f|
43
+ f.headers["x-api-key"] = @api_key
44
+ f.headers["anthropic-version"] = API_VERSION
45
+ f.headers["anthropic-beta"] = FILES_BETA
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "faraday"
5
+
6
+ module RailsAgents
7
+ module Providers
8
+ class Anthropic < Base
9
+ API_VERSION = "2023-06-01"
10
+
11
+ def initialize(api_key: RailsAgents.config.anthropic_api_key)
12
+ raise ConfigurationError, "Set anthropic_api_key in config/initializers/rails_agents.rb" if api_key.to_s.empty?
13
+ @api_key = api_key
14
+ end
15
+
16
+ def chat(messages:, client_tools: [], skills: nil, model:, json: false, &block)
17
+ anthropic_skills = skills&.anthropic_request || {}
18
+ body = build_body(messages:, client_tools:, skills: anthropic_skills, model:)
19
+
20
+ response = connection(anthropic_skills[:beta_headers]).post("messages", body.to_json)
21
+ raise ProviderError, parse_error(response) unless response.success?
22
+
23
+ parse_response(JSON.parse(response.body), json:)
24
+ end
25
+
26
+ def files = @files ||= Files.new(api_key: @api_key)
27
+
28
+ private
29
+
30
+ def build_body(messages:, client_tools:, skills:, model:)
31
+ system, conversation = split_system(messages)
32
+ tools = client_tools.map { |tool|
33
+ {name: tool[:name], description: tool[:description], input_schema: tool[:parameters]}
34
+ }
35
+ tools.concat(skills.fetch(:server_tools, []))
36
+
37
+ {
38
+ model: model,
39
+ max_tokens: 16_384,
40
+ system: system,
41
+ messages: serialize(conversation),
42
+ tools: tools.empty? ? nil : tools,
43
+ container: skills[:container]
44
+ }.compact
45
+ end
46
+
47
+ def parse_response(data, json:)
48
+ content_blocks = data.fetch("content")
49
+ text = Skills::AnthropicContent.extract_text(content_blocks)
50
+ text = "{}" if json && text.blank?
51
+ tool_blocks = content_blocks.select { |block| block["type"] == "tool_use" }
52
+ usage = data.fetch("usage", {})
53
+
54
+ ProviderResponse.new(
55
+ content: text.to_s.empty? ? nil : text,
56
+ tool_calls: tool_blocks.map { |block|
57
+ ToolCall.new(id: block["id"], name: block["name"], arguments: block["input"] || {})
58
+ },
59
+ usage: Usage.new(usage.fetch("input_tokens", 0), usage.fetch("output_tokens", 0)),
60
+ finish_reason: data["stop_reason"],
61
+ content_blocks: content_blocks,
62
+ file_ids: Skills::AnthropicContent.extract_file_ids(content_blocks),
63
+ assistant_raw_content: content_blocks
64
+ )
65
+ end
66
+
67
+ def connection(beta_headers = [])
68
+ headers = {
69
+ "x-api-key" => @api_key,
70
+ "anthropic-version" => API_VERSION,
71
+ "Content-Type" => "application/json"
72
+ }
73
+ headers["anthropic-beta"] = beta_headers.join(",") if beta_headers&.any?
74
+
75
+ Faraday.new(url: "https://api.anthropic.com/v1", headers: headers)
76
+ end
77
+
78
+ def split_system(messages)
79
+ system = messages.select { |message| message.role == :system }.map(&:content).join("\n")
80
+ conversation = messages.reject { |message| message.role == :system }
81
+ [system.to_s.empty? ? nil : system, conversation]
82
+ end
83
+
84
+ def serialize(messages)
85
+ messages.filter_map do |message|
86
+ case message.role
87
+ when :user
88
+ {role: "user", content: message.content}
89
+ when :assistant
90
+ if message.raw_content
91
+ {role: "assistant", content: message.raw_content}
92
+ else
93
+ blocks = []
94
+ blocks << {type: "text", text: message.content} if message.content.present?
95
+ message.tool_calls&.each do |tool_call|
96
+ blocks << {type: "tool_use", id: tool_call.id, name: tool_call.name, input: tool_call.arguments}
97
+ end
98
+ {role: "assistant", content: blocks}
99
+ end
100
+ when :tool
101
+ {role: "user", content: [{type: "tool_result", tool_use_id: message.tool_call_id, content: message.content}]}
102
+ end
103
+ end
104
+ end
105
+
106
+ def parse_error(response)
107
+ JSON.parse(response.body).dig("error", "message") rescue response.body
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAgents
4
+ module Providers
5
+ class Base
6
+ def chat(messages:, tools: [], client_tools: tools, skills: nil, model:, json: false, &block)
7
+ raise NotImplementedError
8
+ end
9
+ end
10
+
11
+ def self.build(name, api_key: nil)
12
+ kwargs = api_key.nil? ? {} : {api_key: api_key}
13
+
14
+ case name.to_sym
15
+ when :openai then OpenAI.new(**kwargs)
16
+ when :anthropic then Anthropic.new(**kwargs)
17
+ when :openrouter then OpenRouter.new(**kwargs)
18
+ when :grok then Grok.new(**kwargs)
19
+ else raise ConfigurationError, "Unknown provider: #{name}. Use: #{Configuration::PROVIDERS.join(', ')}"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAgents
4
+ module Providers
5
+ class Grok < OpenAICompatible
6
+ def initialize(api_key: RailsAgents.config.grok_api_key, base_url: "https://api.x.ai/v1")
7
+ super(
8
+ api_key: api_key,
9
+ base_url: base_url,
10
+ missing_key_message: "Set grok_api_key (or XAI_API_KEY) in config/initializers/rails_agents.rb"
11
+ )
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAgents
4
+ module Providers
5
+ class OpenRouter < OpenAICompatible
6
+ def initialize(api_key: RailsAgents.config.openrouter_api_key, base_url: "https://openrouter.ai/api/v1")
7
+ super(
8
+ api_key: api_key,
9
+ base_url: base_url,
10
+ extra_headers: {
11
+ "HTTP-Referer" => (defined?(Rails) ? "https://#{Rails.application.class.module_parent_name.downcase}.app" : "https://railsagents.dev"),
12
+ "X-Title" => (defined?(Rails) ? Rails.application.class.module_parent_name : "Rails Agents")
13
+ },
14
+ missing_key_message: "Set openrouter_api_key in config/initializers/rails_agents.rb"
15
+ )
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAgents
4
+ module Providers
5
+ class OpenAI < OpenAICompatible
6
+ def initialize(api_key: RailsAgents.config.openai_api_key, base_url: "https://api.openai.com/v1")
7
+ super(
8
+ api_key: api_key,
9
+ base_url: base_url,
10
+ missing_key_message: "Set openai_api_key in config/initializers/rails_agents.rb"
11
+ )
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "faraday"
5
+
6
+ module RailsAgents
7
+ module Providers
8
+ class OpenAICompatible < Base
9
+ def initialize(api_key:, base_url:, extra_headers: {}, missing_key_message:)
10
+ raise ConfigurationError, missing_key_message if api_key.to_s.empty?
11
+ @api_key = api_key
12
+ @base_url = base_url
13
+ @extra_headers = extra_headers
14
+ end
15
+
16
+ def chat(messages:, client_tools: [], skills: nil, model:, json: false, &block)
17
+ tools = client_tools
18
+ body = {
19
+ model: model,
20
+ messages: serialize(messages),
21
+ tools: tools.empty? ? nil : tools.map { |t| {type: "function", function: t} }
22
+ }.compact
23
+ body[:response_format] = {type: "json_object"} if json
24
+
25
+ response = connection.post("chat/completions", body.to_json)
26
+ raise ProviderError, parse_error(response) unless response.success?
27
+
28
+ data = JSON.parse(response.body)
29
+ choice = data.fetch("choices").first
30
+ message = choice.fetch("message")
31
+ usage = data.fetch("usage", {})
32
+
33
+ ProviderResponse.new(
34
+ content: message["content"],
35
+ tool_calls: (message["tool_calls"] || []).map { |tc|
36
+ ToolCall.new(
37
+ id: tc["id"],
38
+ name: tc.dig("function", "name"),
39
+ arguments: JSON.parse(tc.dig("function", "arguments") || "{}")
40
+ )
41
+ },
42
+ usage: Usage.new(usage.fetch("prompt_tokens", 0), usage.fetch("completion_tokens", 0)),
43
+ finish_reason: choice["finish_reason"],
44
+ content_blocks: nil,
45
+ file_ids: [],
46
+ assistant_raw_content: nil
47
+ )
48
+ end
49
+
50
+ private
51
+
52
+ def connection
53
+ @connection ||= Faraday.new(url: @base_url) do |f|
54
+ f.headers["Authorization"] = "Bearer #{@api_key}"
55
+ f.headers["Content-Type"] = "application/json"
56
+ @extra_headers.each { |key, value| f.headers[key] = value }
57
+ end
58
+ end
59
+
60
+ def serialize(messages)
61
+ messages.map do |msg|
62
+ h = {role: msg.role.to_s, content: msg.content}
63
+ if msg.tool_calls&.any?
64
+ h[:tool_calls] = msg.tool_calls.map { |tc|
65
+ {id: tc.id, type: "function", function: {name: tc.name, arguments: tc.arguments.to_json}}
66
+ }
67
+ end
68
+ h[:tool_call_id] = msg.tool_call_id if msg.tool_call_id
69
+ h
70
+ end
71
+ end
72
+
73
+ def parse_error(response)
74
+ JSON.parse(response.body).dig("error", "message") rescue response.body
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_agents"
4
+
5
+ module RailsAgents
6
+ class Railtie < Rails::Railtie
7
+ generators do
8
+ require "generators/rails_agents/install_generator"
9
+ end
10
+
11
+ initializer "rails_agents.autoload", before: :set_autoload_paths do |app|
12
+ agents_path = app.root.join("app/agents")
13
+ next unless agents_path.exist?
14
+
15
+ app.autoloaders.main.push_dir(agents_path.to_s)
16
+
17
+ tools_path = agents_path.join("tools")
18
+ app.autoloaders.main.collapse(tools_path.to_s) if tools_path.exist?
19
+ end
20
+
21
+ rake_tasks do
22
+ load "rails_agents/tasks.rb" if File.exist?(File.expand_path("tasks.rb", __dir__))
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAgents
4
+ Result = Data.define(:output, :data, :files, :messages, :usage, :success, :error, :content_blocks) do
5
+ def self.ok(output:, files: [], data: nil, messages: [], usage: Usage.new(0, 0), content_blocks: nil)
6
+ new(output:, data:, files:, messages:, usage:, success: true, error: nil, content_blocks:)
7
+ end
8
+
9
+ def self.fail(error:, messages: [], usage: Usage.new(0, 0), content_blocks: nil)
10
+ new(output: nil, data: nil, files: [], messages:, usage:, success: false, error:, content_blocks:)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAgents
4
+ class Runner
5
+ DEFAULT_TURNS = 12
6
+
7
+ def initialize(agent_class, input:, context: {}, callbacks: {}, parse_json: false, provider: nil, model: nil)
8
+ @agent = agent_class
9
+ @input = input
10
+ @context = context
11
+ @callbacks = callbacks
12
+ @parse_json = parse_json
13
+ @skills = agent_class.skill_set
14
+ @tools = agent_class.tool_set
15
+ @provider = provider || agent_class.provider_client
16
+ @model_override = model
17
+ @server_tool_names = @skills.server_tool_names
18
+ @save_files_to = context[:save_files_to]
19
+ @messages = []
20
+ end
21
+
22
+ def call
23
+ emit(:start, agent: @agent.name, input: @input, context: @context)
24
+
25
+ @messages << Message.system(@agent.render_instructions(@context))
26
+ @messages << Message.user(format_input(@input))
27
+
28
+ turns = 0
29
+ total_usage = Usage.new(0, 0)
30
+ last_response = nil
31
+
32
+ loop do
33
+ turns += 1
34
+ raise Error, "Turn limit reached" if turns > @agent.max_turns
35
+
36
+ emit(:turn_start, turn: turns)
37
+
38
+ response = @provider.chat(
39
+ messages: @messages,
40
+ client_tools: @tools.definitions,
41
+ skills: @skills,
42
+ model: @model_override || @agent.resolved_model
43
+ )
44
+ last_response = response
45
+
46
+ total_usage = Usage.new(
47
+ total_usage.input_tokens + response.usage.input_tokens,
48
+ total_usage.output_tokens + response.usage.output_tokens
49
+ )
50
+
51
+ emit(:turn_complete, turn: turns, usage: response.usage, tool_calls: response.tool_calls.map(&:name))
52
+
53
+ client_tool_calls = response.tool_calls.reject { |tool_call| @server_tool_names.include?(tool_call.name) }
54
+
55
+ if client_tool_calls.any?
56
+ @messages << Message.assistant(
57
+ response.content,
58
+ tool_calls: client_tool_calls,
59
+ raw_content: response.assistant_raw_content
60
+ )
61
+ client_tool_calls.each do |tool_call|
62
+ emit(:tool_start, name: tool_call.name, arguments: tool_call.arguments)
63
+ result = @tools.execute(tool_call.name, tool_call.arguments)
64
+ emit(:tool_finish, name: tool_call.name, result: result)
65
+ @messages << Message.tool(result.to_json, tool_call_id: tool_call.id)
66
+ end
67
+ next
68
+ end
69
+
70
+ files = download_files(response.file_ids)
71
+ data = parse_structured_output(response.content)
72
+ result = Result.ok(
73
+ output: response.content,
74
+ data: data,
75
+ files: files,
76
+ messages: @messages,
77
+ usage: total_usage,
78
+ content_blocks: response.content_blocks
79
+ )
80
+ emit(:finish, result: result)
81
+ return result
82
+ end
83
+ rescue StandardError => e
84
+ result = Result.fail(error: e.message, messages: @messages, usage: Usage.new(0, 0))
85
+ emit(:error, error: e.message, result: result)
86
+ result
87
+ end
88
+
89
+ private
90
+
91
+ def emit(event, payload = {})
92
+ @callbacks[event]&.call(payload)
93
+ instrument(event, payload)
94
+ end
95
+
96
+ def instrument(event, payload)
97
+ return unless defined?(ActiveSupport::Notifications)
98
+
99
+ ActiveSupport::Notifications.instrument(
100
+ "rails_agents.#{event}",
101
+ payload.merge(agent: @agent.name)
102
+ )
103
+ end
104
+
105
+ def parse_structured_output(content)
106
+ return nil unless @parse_json
107
+ return nil if content.nil? || content.strip.empty?
108
+
109
+ JSON.parse(content)
110
+ rescue JSON::ParserError
111
+ extract_json_object(content)
112
+ end
113
+
114
+ def extract_json_object(content)
115
+ match = content.match(/\{.*\}/m)
116
+ return nil unless match
117
+
118
+ JSON.parse(match[0])
119
+ rescue JSON::ParserError
120
+ nil
121
+ end
122
+
123
+ def format_input(input)
124
+ case input
125
+ when String then input
126
+ when Hash then input.to_json
127
+ else input.to_s
128
+ end
129
+ end
130
+
131
+ def download_files(file_ids)
132
+ return [] if file_ids.empty?
133
+ return [] unless @agent.resolved_provider == :anthropic
134
+ return [] unless RailsAgents.config.anthropic_auto_download_files
135
+ return [] unless @provider.respond_to?(:files)
136
+
137
+ directory = @save_files_to || RailsAgents.config.anthropic_files_directory!
138
+ FileUtils.mkdir_p(directory)
139
+
140
+ file_ids.map do |file_id|
141
+ generated = @provider.files.download(file_id)
142
+ generated.save(directory)
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAgents
4
+ class Skill
5
+ SERVER = :server
6
+ PORTABLE = :portable
7
+ ANTHROPIC_DOCUMENT = :anthropic_document
8
+
9
+ attr_reader :name, :kind, :anthropic_tool, :anthropic_skill, :requires, :portable_tool
10
+
11
+ def initialize(name:, kind:, anthropic_tool: nil, anthropic_skill: nil, requires: [], portable_tool: nil)
12
+ @name = name.to_sym
13
+ @kind = kind
14
+ @anthropic_tool = anthropic_tool
15
+ @anthropic_skill = anthropic_skill
16
+ @requires = requires.map(&:to_sym)
17
+ @portable_tool = portable_tool
18
+ end
19
+
20
+ def server? = kind == SERVER
21
+ def portable? = kind == PORTABLE
22
+ def anthropic_document? = kind == ANTHROPIC_DOCUMENT
23
+ end
24
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAgents
4
+ SkillDeclaration = Data.define(:key, :options) do
5
+ def custom? = key.to_s.start_with?("skill_")
6
+ def name = custom? ? key.to_s : key.to_sym
7
+ end
8
+ end