rails-agent-stack 0.1.1
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/.github/workflows/README.md +16 -0
- data/.github/workflows/ci.yml +32 -0
- data/.gitignore +8 -0
- data/.rspec +3 -0
- data/AGENTS.md +18 -0
- data/CHANGELOG.md +40 -0
- data/Gemfile +15 -0
- data/MIT-LICENSE +21 -0
- data/README.md +228 -0
- data/Rakefile +8 -0
- data/app/assets/images/rails_agents/kip-logo.png +0 -0
- data/app/assets/images/rails_agents/kip-waving.png +0 -0
- data/app/assets/stylesheets/rails_agents/application.css +411 -0
- data/app/controllers/rails_agents/application_controller.rb +25 -0
- data/app/controllers/rails_agents/connect_controller.rb +36 -0
- data/app/controllers/rails_agents/dashboard_controller.rb +70 -0
- data/app/controllers/rails_agents/handshake_controller.rb +55 -0
- data/app/controllers/rails_agents/open_wire_inbound_controller.rb +153 -0
- data/app/controllers/rails_agents/pulls_controller.rb +42 -0
- data/app/controllers/rails_agents/registrations_controller.rb +96 -0
- data/app/controllers/rails_agents/schema_controller.rb +123 -0
- data/app/controllers/rails_agents/sessions_controller.rb +76 -0
- data/app/controllers/rails_agents/tool_bridge_controller.rb +67 -0
- data/app/views/layouts/rails_agents/application.html.erb +18 -0
- data/app/views/rails_agents/dashboard/show.html.erb +139 -0
- data/app/views/rails_agents/registrations/new.html.erb +83 -0
- data/app/views/rails_agents/sessions/new.html.erb +73 -0
- data/app/views/rails_agents/shared/_auth_brand.html.erb +20 -0
- data/config/routes.rb +34 -0
- data/docs/AGENTS.md +220 -0
- data/docs/PRD.md +89 -0
- data/docs/assets/hero-banner.png +0 -0
- data/examples/support/agent.rb +14 -0
- data/examples/support/channels/.keep +0 -0
- data/examples/support/evals/smoke.yml +4 -0
- data/examples/support/knowledge/shipping.md +1 -0
- data/examples/support/memory.rb +3 -0
- data/examples/support/prompt.md +9 -0
- data/examples/support/skills/.keep +0 -0
- data/examples/support/tools/.keep +0 -0
- data/exe/rails-agents +6 -0
- data/lib/generators/rails_agents/agent/agent_generator.rb +80 -0
- data/lib/generators/rails_agents/agent/templates/agent.rb.tt +14 -0
- data/lib/generators/rails_agents/agent/templates/agent_database.rb.tt +42 -0
- data/lib/generators/rails_agents/agent/templates/channels/slack.rb.tt +8 -0
- data/lib/generators/rails_agents/agent/templates/evals/smoke.yml.tt +4 -0
- data/lib/generators/rails_agents/agent/templates/imports.yml.tt +9 -0
- data/lib/generators/rails_agents/agent/templates/memory.rb.tt +6 -0
- data/lib/generators/rails_agents/agent/templates/prompt.md.tt +10 -0
- data/lib/generators/rails_agents/agent/templates/prompt_database.md.tt +14 -0
- data/lib/generators/rails_agents/install/INSTALL +10 -0
- data/lib/generators/rails_agents/install/install_generator.rb +72 -0
- data/lib/generators/rails_agents/install/templates/AGENTS.md.tt +46 -0
- data/lib/generators/rails_agents/install/templates/initializer.rb.tt +8 -0
- data/lib/generators/rails_agents/install/templates/library_README.md.tt +28 -0
- data/lib/generators/rails_agents/install/templates/library_manifest.yml.tt +5 -0
- data/lib/generators/rails_agents/install/templates/rails_agents_autoload.rb.tt +10 -0
- data/lib/rails-agent-stack.rb +4 -0
- data/lib/rails_agents/background_agent.rb +15 -0
- data/lib/rails_agents/base.rb +294 -0
- data/lib/rails_agents/chat_agent.rb +15 -0
- data/lib/rails_agents/cli.rb +245 -0
- data/lib/rails_agents/client.rb +216 -0
- data/lib/rails_agents/configuration.rb +41 -0
- data/lib/rails_agents/creation_agent.rb +10 -0
- data/lib/rails_agents/credentials_writer.rb +38 -0
- data/lib/rails_agents/engine.rb +19 -0
- data/lib/rails_agents/knowledge_agent.rb +10 -0
- data/lib/rails_agents/library_imports.rb +163 -0
- data/lib/rails_agents/local_auth.rb +56 -0
- data/lib/rails_agents/local_sync.rb +113 -0
- data/lib/rails_agents/monitoring_agent.rb +10 -0
- data/lib/rails_agents/open_wire_adapter.rb +31 -0
- data/lib/rails_agents/operations_agent.rb +10 -0
- data/lib/rails_agents/railtie.rb +14 -0
- data/lib/rails_agents/tasks.rake +8 -0
- data/lib/rails_agents/version.rb +5 -0
- data/lib/rails_agents/workflow_agent.rb +11 -0
- data/lib/rails_agents.rb +57 -0
- data/rails-agent-stack.gemspec +49 -0
- data/rails-agents.code-workspace +25 -0
- data/spec/generators/agent_generator_spec.rb +36 -0
- data/spec/rails_agents/base_spec.rb +202 -0
- data/spec/rails_agents/client_spec.rb +121 -0
- data/spec/rails_agents/configuration_spec.rb +55 -0
- data/spec/rails_agents/library_imports_spec.rb +80 -0
- data/spec/rails_agents/local_sync_spec.rb +63 -0
- data/spec/rails_agents/taxonomy_spec.rb +52 -0
- data/spec/spec_helper.rb +27 -0
- metadata +229 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "active_support/core_ext/string/inflections"
|
|
5
|
+
|
|
6
|
+
module RailsAgents
|
|
7
|
+
# Prefer product-facing typed subclasses: KnowledgeAgent, WorkflowAgent,
|
|
8
|
+
# OperationsAgent, or MonitoringAgent.
|
|
9
|
+
class Base
|
|
10
|
+
class << self
|
|
11
|
+
attr_reader :agent_name, :model_setting, :model_provider, :model_credential,
|
|
12
|
+
:memory_setting, :memory_provider, :memory_recall, :knowledge_glob,
|
|
13
|
+
:tool_definitions, :skill_definitions, :channel_definitions
|
|
14
|
+
|
|
15
|
+
def agent_kind
|
|
16
|
+
:base
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def inherited(subclass)
|
|
20
|
+
super
|
|
21
|
+
subclass.instance_variable_set(:@tool_definitions, {})
|
|
22
|
+
subclass.instance_variable_set(:@skill_definitions, {})
|
|
23
|
+
subclass.instance_variable_set(:@channel_definitions, [])
|
|
24
|
+
subclass.instance_variable_set(:@channel_via, {})
|
|
25
|
+
subclass.instance_variable_set(:@channel_message_handler, nil)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# model :gpt_5_mini, provider: :openai, credential: :company_openai
|
|
29
|
+
# model :auto # legacy cloud-routed default (still supported)
|
|
30
|
+
def model(value, provider: nil, credential: nil)
|
|
31
|
+
@model_setting = value
|
|
32
|
+
@model_provider = provider
|
|
33
|
+
@model_credential = credential
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# memory :conversation, provider: :mem0, recall: 5
|
|
37
|
+
def memory(value, provider: nil, recall: nil)
|
|
38
|
+
@memory_setting = value
|
|
39
|
+
@memory_provider = provider
|
|
40
|
+
@memory_recall = recall
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def knowledge_from(glob)
|
|
44
|
+
@knowledge_glob = glob
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def tool(name, &block)
|
|
48
|
+
@tool_definitions ||= {}
|
|
49
|
+
@tool_definitions[name.to_sym] = block
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def skill(name, from:)
|
|
53
|
+
@skill_definitions ||= {}
|
|
54
|
+
@skill_definitions[name.to_sym] = from
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Declares a SaaS connector (manifest lives under connectors/).
|
|
58
|
+
# `plugin` remains as a deprecated alias.
|
|
59
|
+
def connector(name, **opts)
|
|
60
|
+
@connector_definitions ||= {}
|
|
61
|
+
@connector_definitions[name.to_sym] = opts
|
|
62
|
+
end
|
|
63
|
+
alias_method :plugin, :connector
|
|
64
|
+
|
|
65
|
+
def channel(kind, via: nil)
|
|
66
|
+
@channel_definitions ||= []
|
|
67
|
+
@channel_definitions << kind.to_sym
|
|
68
|
+
@channel_via ||= {}
|
|
69
|
+
@channel_via[kind.to_sym] = via.to_sym if via
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def channel_via(kind)
|
|
73
|
+
(@channel_via || {})[kind.to_sym]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Callback for Open-Wire (and future channel) inbound messages.
|
|
77
|
+
# on_channel_message { |msg| … }
|
|
78
|
+
def on_channel_message(&block)
|
|
79
|
+
@channel_message_handler = block
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def channel_message_handler
|
|
83
|
+
@channel_message_handler
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def agent_directory
|
|
87
|
+
@agent_directory ||= resolve_agent_directory
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def run(message = nil, session_id: nil, client: nil, **inputs)
|
|
91
|
+
text = coerce_run_message(message, inputs)
|
|
92
|
+
new(client: client).run(text, session_id: session_id)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def deploy(client: nil)
|
|
96
|
+
new(client: client).deploy
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def sync_knowledge(client: nil)
|
|
100
|
+
new(client: client).sync_knowledge
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def coerce_run_message(message, inputs)
|
|
104
|
+
if inputs.any?
|
|
105
|
+
if !message.nil?
|
|
106
|
+
raise ArgumentError, "pass either a message string or keyword inputs, not both"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
inputs.map { |key, value| "#{key}: #{value}" }.join("\n")
|
|
110
|
+
elsif message.nil?
|
|
111
|
+
raise ArgumentError, "message is required (or pass keyword inputs like order_id:)"
|
|
112
|
+
else
|
|
113
|
+
message.to_s
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
private :coerce_run_message
|
|
117
|
+
|
|
118
|
+
def resolve_agent_directory
|
|
119
|
+
caller_path = Pathname.new(caller_locations(1, 1).first.absolute_path)
|
|
120
|
+
caller_path.dirname
|
|
121
|
+
end
|
|
122
|
+
private :resolve_agent_directory
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def initialize(client: nil)
|
|
126
|
+
@client = client || Client.new
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def run(message = nil, session_id: nil, **inputs)
|
|
130
|
+
text = self.class.send(:coerce_run_message, message, inputs)
|
|
131
|
+
response = @client.create_run(
|
|
132
|
+
agent: agent_identifier,
|
|
133
|
+
message: text,
|
|
134
|
+
session_id: session_id,
|
|
135
|
+
metadata: agent_metadata
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
RunResult.new(response, client: @client)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def deploy
|
|
142
|
+
@client.deploy(agent: agent_identifier, bundle_path: self.class.agent_directory.to_s)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def sync_knowledge
|
|
146
|
+
paths = knowledge_paths
|
|
147
|
+
@client.sync_knowledge(agent: agent_identifier, paths: paths)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def agent_metadata
|
|
151
|
+
{
|
|
152
|
+
kind: self.class.agent_kind,
|
|
153
|
+
model: model_metadata,
|
|
154
|
+
memory: memory_metadata,
|
|
155
|
+
tools: self.class.tool_definitions&.keys || [],
|
|
156
|
+
skills: self.class.skill_definitions || {},
|
|
157
|
+
channels: self.class.channel_definitions || []
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def model_metadata
|
|
162
|
+
setting = self.class.model_setting
|
|
163
|
+
provider = self.class.model_provider
|
|
164
|
+
credential = self.class.model_credential
|
|
165
|
+
|
|
166
|
+
if provider.nil? && credential.nil?
|
|
167
|
+
setting
|
|
168
|
+
else
|
|
169
|
+
{
|
|
170
|
+
name: setting,
|
|
171
|
+
provider: provider,
|
|
172
|
+
credential: credential
|
|
173
|
+
}.compact
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def memory_metadata
|
|
178
|
+
setting = self.class.memory_setting
|
|
179
|
+
provider = self.class.memory_provider
|
|
180
|
+
recall = self.class.memory_recall
|
|
181
|
+
|
|
182
|
+
if provider.nil? && recall.nil?
|
|
183
|
+
setting
|
|
184
|
+
else
|
|
185
|
+
{
|
|
186
|
+
type: setting,
|
|
187
|
+
provider: provider,
|
|
188
|
+
recall: recall
|
|
189
|
+
}.compact
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def agent_identifier
|
|
194
|
+
if self.class.name && !self.class.name.empty?
|
|
195
|
+
self.class.name.underscore
|
|
196
|
+
else
|
|
197
|
+
self.class.agent_directory.basename.to_s
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def knowledge_paths
|
|
202
|
+
glob = self.class.knowledge_glob
|
|
203
|
+
return [] unless glob
|
|
204
|
+
|
|
205
|
+
dir = self.class.agent_directory
|
|
206
|
+
local = Dir.glob(dir.join(glob).to_s).map do |path|
|
|
207
|
+
Pathname.new(path).relative_path_from(dir).to_s
|
|
208
|
+
end
|
|
209
|
+
(local + LibraryImports.new(dir).knowledge_paths).uniq
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
class RunResult
|
|
213
|
+
attr_reader :response, :client
|
|
214
|
+
|
|
215
|
+
def initialize(response, client:)
|
|
216
|
+
@response = response
|
|
217
|
+
@client = client
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def run_id
|
|
221
|
+
response["id"] ||
|
|
222
|
+
response["run_id"] ||
|
|
223
|
+
dig_data("id")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def stream(&block)
|
|
227
|
+
client.stream_run(run_id, &block)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Prefer the sync create_run body; otherwise drain the SSE stream.
|
|
231
|
+
# Legacy alias for output_text (Markdown final answer).
|
|
232
|
+
def output
|
|
233
|
+
text = output_text
|
|
234
|
+
return text if text.is_a?(String) && !text.empty?
|
|
235
|
+
|
|
236
|
+
final = nil
|
|
237
|
+
chunks = []
|
|
238
|
+
stream do |event|
|
|
239
|
+
# Prefer terminal / normalized payloads (done / output events).
|
|
240
|
+
if event["output_text"].is_a?(String) && !event["output_text"].empty?
|
|
241
|
+
final = event["output_text"]
|
|
242
|
+
elsif event["output"].is_a?(String) && !event["output"].empty? &&
|
|
243
|
+
(event.key?("items") || event.key?("format") || event.key?("status"))
|
|
244
|
+
final = event["output"]
|
|
245
|
+
else
|
|
246
|
+
piece = event["content"] || event["text"]
|
|
247
|
+
chunks << piece if piece.is_a?(String) && !piece.empty?
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
final || chunks.join
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Eve-aligned: final assistant Markdown (provider-agnostic).
|
|
254
|
+
def output_text
|
|
255
|
+
response["output_text"] ||
|
|
256
|
+
dig_data("output_text") ||
|
|
257
|
+
response["output"] ||
|
|
258
|
+
dig_data("output")
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Optional structured final payload (when a schema/result was requested).
|
|
262
|
+
def output_data
|
|
263
|
+
response["output_data"] || dig_data("output_data")
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Typed turn items: message (+ optional result). Tool trail lives on #trace.
|
|
267
|
+
def items
|
|
268
|
+
response["items"] || dig_data("items") || []
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def format
|
|
272
|
+
response["format"] || dig_data("format") || "markdown"
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def status
|
|
276
|
+
response["status"] || dig_data("status")
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def trace
|
|
280
|
+
response["trace"] || dig_data("trace")
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
private
|
|
284
|
+
|
|
285
|
+
def dig_data(key)
|
|
286
|
+
data = response["data"]
|
|
287
|
+
data.is_a?(Hash) ? data[key] : nil
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Alias for PRD compatibility
|
|
293
|
+
RailsAgent = RailsAgents unless defined?(RailsAgent)
|
|
294
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAgents
|
|
4
|
+
# @deprecated Use {KnowledgeAgent} for conversational / Q&A agents.
|
|
5
|
+
class ChatAgent < KnowledgeAgent
|
|
6
|
+
def self.inherited(subclass)
|
|
7
|
+
super
|
|
8
|
+
warn "[RailsAgents] ChatAgent is deprecated; use KnowledgeAgent instead.", uplevel: 1
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.agent_kind
|
|
12
|
+
:knowledge
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require "io/console"
|
|
5
|
+
|
|
6
|
+
module RailsAgents
|
|
7
|
+
class CLI < Thor
|
|
8
|
+
package_name "rails-agents"
|
|
9
|
+
|
|
10
|
+
desc "install", "Mount the engine and write the initializer"
|
|
11
|
+
def install
|
|
12
|
+
exec_rails_generator("rails_agents:install")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc "new NAME", "Scaffold a new agent directory"
|
|
16
|
+
def new(name)
|
|
17
|
+
exec_rails_generator("rails_agents:agent", name)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc "login", "Sign in to Rails Agent Cloud and print export lines for your API key"
|
|
21
|
+
method_option :email, type: :string, aliases: "-e"
|
|
22
|
+
method_option :workspace, type: :string, aliases: "-w"
|
|
23
|
+
def login
|
|
24
|
+
email = options[:email] || ask("Email:")
|
|
25
|
+
password = $stdin.tty? ? $stdin.getpass("Password: ") : ask("Password:")
|
|
26
|
+
workspace = options[:workspace]
|
|
27
|
+
|
|
28
|
+
response = Client.new.handshake(
|
|
29
|
+
email: email,
|
|
30
|
+
password: password,
|
|
31
|
+
workspace: workspace
|
|
32
|
+
)
|
|
33
|
+
data = response["data"] || response
|
|
34
|
+
api_key = data["api_key"] || data["apiKey"]
|
|
35
|
+
api_key = api_key["token"] if api_key.is_a?(Hash)
|
|
36
|
+
project_id = data["project_id"] || data["projectId"] || data.dig("project", "id")
|
|
37
|
+
dashboard_url = data["dashboard_url"] || data["dashboardUrl"]
|
|
38
|
+
|
|
39
|
+
say ""
|
|
40
|
+
say "Signed in to Rails Agent Cloud."
|
|
41
|
+
say "Cloud host is fixed — only these belong in .env (or production env):"
|
|
42
|
+
say ""
|
|
43
|
+
say "export RAILS_AGENTS_API_KEY=#{api_key}" if api_key.present?
|
|
44
|
+
say "export RAILS_AGENTS_PROJECT_ID=#{project_id}" if project_id.present?
|
|
45
|
+
say ""
|
|
46
|
+
say "Get or rotate keys anytime: https://cloud.rails-agent.com/dashboard/keys"
|
|
47
|
+
say "Then visit /agents in your Rails app."
|
|
48
|
+
if dashboard_url.present?
|
|
49
|
+
say "Dashboard: #{dashboard_url.to_s.split("?").first}"
|
|
50
|
+
end
|
|
51
|
+
rescue Client::Error => e
|
|
52
|
+
say_error e.message
|
|
53
|
+
exit 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
map "run" => :execute
|
|
57
|
+
|
|
58
|
+
desc "execute NAME [MESSAGE]", "Run an agent against the cloud runtime"
|
|
59
|
+
method_option :session, type: :string, desc: "Session ID for conversation memory"
|
|
60
|
+
def execute(name, message = nil)
|
|
61
|
+
agent_class = load_agent_class(name)
|
|
62
|
+
message ||= $stdin.tty? ? Thor::Shell::Basic.new.ask("Message:") : $stdin.read
|
|
63
|
+
|
|
64
|
+
result = agent_class.run(message, session_id: options[:session])
|
|
65
|
+
say result.output
|
|
66
|
+
rescue LoadError, NameError => e
|
|
67
|
+
say_error "Could not load agent #{name}: #{e.message}"
|
|
68
|
+
exit 1
|
|
69
|
+
rescue Client::Error => e
|
|
70
|
+
say_error e.message
|
|
71
|
+
exit 1
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
desc "deploy [NAME]", "Deploy agent bundle(s) to Rails Agent Cloud"
|
|
75
|
+
def deploy(name = nil)
|
|
76
|
+
agents = name ? [name] : discover_agent_names
|
|
77
|
+
agents.each do |agent_name|
|
|
78
|
+
agent_class = load_agent_class(agent_name)
|
|
79
|
+
response = agent_class.deploy
|
|
80
|
+
say "Deployed #{agent_name}: #{response["id"] || response["status"] || "ok"}"
|
|
81
|
+
end
|
|
82
|
+
rescue Client::Error => e
|
|
83
|
+
say_error e.message
|
|
84
|
+
exit 1
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
desc "sync NAME", "Push local agent files to the cloud"
|
|
88
|
+
def sync(name)
|
|
89
|
+
agent_class = load_agent_class(name)
|
|
90
|
+
dir = agent_class.agent_directory
|
|
91
|
+
root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
|
|
92
|
+
files = Dir[File.join(dir, "**", "*")].select { |p| File.file?(p) }.map do |path|
|
|
93
|
+
rel = path.sub(%r{\A#{Regexp.escape(root)}/?}, "")
|
|
94
|
+
rel = rel.sub(%r{\A.*?app/agents/}, "app/agents/") if rel.include?("app/agents/")
|
|
95
|
+
{ "path" => rel, "content" => File.read(path) }
|
|
96
|
+
end
|
|
97
|
+
imported = LibraryImports.new(dir).virtual_files
|
|
98
|
+
local_paths = files.to_h { |file| [file["path"], true] }
|
|
99
|
+
conflicts = imported.filter_map do |file|
|
|
100
|
+
file["path"] if local_paths[file["path"]]
|
|
101
|
+
end
|
|
102
|
+
unless conflicts.empty?
|
|
103
|
+
raise LibraryImports::Error,
|
|
104
|
+
"Library imports conflict with agent-local files: #{conflicts.join(", ")}"
|
|
105
|
+
end
|
|
106
|
+
files.concat(
|
|
107
|
+
imported.map { |file| { "path" => file["path"], "content" => file["content"] } }
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
Client.new.sync_files(agent: name, files: files)
|
|
111
|
+
suffix = imported.empty? ? "" : " (#{imported.size} from app/agents_library)"
|
|
112
|
+
say "Synced #{files.size} files for #{name}#{suffix}"
|
|
113
|
+
rescue LibraryImports::Error => e
|
|
114
|
+
say_error e.message
|
|
115
|
+
exit 1
|
|
116
|
+
rescue Client::Error => e
|
|
117
|
+
say_error e.message
|
|
118
|
+
exit 1
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
desc "pull AGENT_ID_OR_SLUG", "[deprecated] Pull agent files from cloud into app/agents/<slug>/"
|
|
122
|
+
def pull(agent_id)
|
|
123
|
+
warn "[RailsAgents] `rails-agents pull` is deprecated; scaffold locally and use `rails-agents sync`."
|
|
124
|
+
result = LocalSync.new.pull!(agent_id)
|
|
125
|
+
say "Pulled #{result["count"]} files into app/agents/#{result["slug"]}/"
|
|
126
|
+
Array(result["written"]).each { |path| say " #{path}" }
|
|
127
|
+
rescue LocalSync::Error, Client::Error => e
|
|
128
|
+
say_error e.message
|
|
129
|
+
exit 1
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
desc "packages QUERY", "Search Skills.sh / APM / Smithery packages"
|
|
133
|
+
method_option :registry, type: :string, default: "all",
|
|
134
|
+
desc: "all | skills_sh | apm | smithery"
|
|
135
|
+
method_option :limit, type: :numeric, default: 12
|
|
136
|
+
def packages(query)
|
|
137
|
+
response = Client.new.search_packages(
|
|
138
|
+
query: query,
|
|
139
|
+
registry: options[:registry],
|
|
140
|
+
limit: options[:limit]
|
|
141
|
+
)
|
|
142
|
+
rows = response["data"] || []
|
|
143
|
+
if rows.empty?
|
|
144
|
+
say "No packages found for #{query.inspect}"
|
|
145
|
+
return
|
|
146
|
+
end
|
|
147
|
+
rows.each do |row|
|
|
148
|
+
installs = row["installs"] ? " (#{row["installs"]} installs)" : ""
|
|
149
|
+
say "#{row["registry"]} #{row["name"]}#{installs}"
|
|
150
|
+
say " #{row["id"]}"
|
|
151
|
+
say " #{row["description"]}"
|
|
152
|
+
say ""
|
|
153
|
+
end
|
|
154
|
+
rescue Client::Error => e
|
|
155
|
+
say_error e.message
|
|
156
|
+
exit 1
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
desc "add-package ID", "Install a registry package onto an agent (and Library)"
|
|
160
|
+
method_option :agent, type: :string, required: true, aliases: "-a"
|
|
161
|
+
method_option :registry, type: :string, required: true,
|
|
162
|
+
desc: "skills_sh | apm | smithery"
|
|
163
|
+
method_option :name, type: :string
|
|
164
|
+
method_option :source, type: :string
|
|
165
|
+
method_option :description, type: :string
|
|
166
|
+
def add_package(id)
|
|
167
|
+
payload = {
|
|
168
|
+
registry: options[:registry],
|
|
169
|
+
id: id,
|
|
170
|
+
name: options[:name] || id.split("/").last,
|
|
171
|
+
source: options[:source] || id.sub(/\A(skills_sh|apm|smithery):/, ""),
|
|
172
|
+
description: options[:description],
|
|
173
|
+
agent_id: options[:agent],
|
|
174
|
+
save_to_library: true
|
|
175
|
+
}.compact
|
|
176
|
+
response = Client.new.install_package(payload)
|
|
177
|
+
data = response["data"] || {}
|
|
178
|
+
pkg = data["package"] || {}
|
|
179
|
+
say "Installed #{pkg["name"] || id}"
|
|
180
|
+
Array(data["paths"]).each { |path| say " #{path}" }
|
|
181
|
+
say "Then run: bundle exec rails-agents sync #{options[:agent]}"
|
|
182
|
+
rescue Client::Error => e
|
|
183
|
+
say_error e.message
|
|
184
|
+
exit 1
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
desc "logs [NAME]", "Fetch recent run logs from the cloud"
|
|
188
|
+
method_option :limit, type: :numeric, default: 50
|
|
189
|
+
def logs(name = nil)
|
|
190
|
+
print_json Client.new.logs(agent: name, limit: options[:limit])
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
desc "traces [NAME]", "Fetch recent traces from the cloud"
|
|
194
|
+
method_option :limit, type: :numeric, default: 50
|
|
195
|
+
def traces(name = nil)
|
|
196
|
+
print_json Client.new.traces(agent: name, limit: options[:limit])
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
desc "evals [NAME]", "Fetch eval results from the cloud"
|
|
200
|
+
def evals(name = nil)
|
|
201
|
+
print_json Client.new.evals(agent: name)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
default_task :help
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
def exec_rails_generator(generator, *args)
|
|
209
|
+
if File.exist?("bin/rails")
|
|
210
|
+
exec "bin/rails", "generate", generator, *args
|
|
211
|
+
else
|
|
212
|
+
say_error "Run this command from your Rails app root (bin/rails not found)."
|
|
213
|
+
exit 1
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def load_agent_class(name)
|
|
218
|
+
path = Rails.root.join("app/agents/#{name}/agent.rb") if defined?(Rails)
|
|
219
|
+
path ||= File.expand_path("app/agents/#{name}/agent.rb", Dir.pwd)
|
|
220
|
+
|
|
221
|
+
require path
|
|
222
|
+
classify(name).constantize
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def discover_agent_names
|
|
226
|
+
base = defined?(Rails) ? Rails.root.join("app/agents") : Pathname.new(Dir.pwd).join("app/agents")
|
|
227
|
+
return [] unless base.directory?
|
|
228
|
+
|
|
229
|
+
base.children.select(&:directory?).map { |d| d.basename.to_s }
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def classify(name)
|
|
233
|
+
name.to_s.split(%r{[_\-/]}).map(&:capitalize).join
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def print_json(data)
|
|
237
|
+
require "json"
|
|
238
|
+
say JSON.pretty_generate(data)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def say_error(message)
|
|
242
|
+
say message, :red
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|