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,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open_wire"
|
|
4
|
+
|
|
5
|
+
module RailsAgents
|
|
6
|
+
# Open-Wire channel adapter — thin wrapper so agents don't talk HTTP directly.
|
|
7
|
+
module OpenWireAdapter
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def client(base_url: nil, api_key: nil)
|
|
11
|
+
::OpenWire::Client.new(base_url: base_url, api_key: api_key)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def verify_inbound!(secret:, body:, headers:)
|
|
15
|
+
::OpenWire::Webhook.verify_and_parse!(
|
|
16
|
+
secret: secret,
|
|
17
|
+
body: body,
|
|
18
|
+
headers: headers
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reply!(installation_id:, to:, text:, thread_id: nil, client: nil)
|
|
23
|
+
(client || self.client).send_message(
|
|
24
|
+
installation_id: installation_id,
|
|
25
|
+
to: to,
|
|
26
|
+
text: text,
|
|
27
|
+
thread_id: thread_id
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAgents
|
|
4
|
+
class Railtie < Rails::Railtie
|
|
5
|
+
rake_tasks do
|
|
6
|
+
load "rails_agents/tasks.rake"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
generators do
|
|
10
|
+
require "generators/rails_agents/install/install_generator"
|
|
11
|
+
require "generators/rails_agents/agent/agent_generator"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAgents
|
|
4
|
+
# Multi-step orchestration agents (onboarding, lead qualification, pipelines).
|
|
5
|
+
# Prefer a stable +session_id+ so each step continues the same run context.
|
|
6
|
+
class WorkflowAgent < Base
|
|
7
|
+
def self.agent_kind
|
|
8
|
+
:workflow
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
data/lib/rails_agents.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails_agents/version"
|
|
4
|
+
require "rails_agents/configuration"
|
|
5
|
+
require "rails_agents/client"
|
|
6
|
+
require "rails_agents/credentials_writer"
|
|
7
|
+
require "rails_agents/local_auth"
|
|
8
|
+
require "rails_agents/local_sync"
|
|
9
|
+
require "rails_agents/library_imports"
|
|
10
|
+
require "rails_agents/base"
|
|
11
|
+
require "rails_agents/knowledge_agent"
|
|
12
|
+
require "rails_agents/creation_agent"
|
|
13
|
+
require "rails_agents/workflow_agent"
|
|
14
|
+
require "rails_agents/operations_agent"
|
|
15
|
+
require "rails_agents/monitoring_agent"
|
|
16
|
+
require "rails_agents/chat_agent"
|
|
17
|
+
require "rails_agents/background_agent"
|
|
18
|
+
require "rails_agents/cli"
|
|
19
|
+
|
|
20
|
+
begin
|
|
21
|
+
require "open_wire"
|
|
22
|
+
require "rails_agents/open_wire_adapter"
|
|
23
|
+
rescue LoadError
|
|
24
|
+
# open-wire gem optional until path/RubyGems dependency is installed
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if defined?(Rails::Engine)
|
|
28
|
+
require "rails_agents/engine"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if defined?(Rails::Railtie)
|
|
32
|
+
require "rails_agents/railtie"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module RailsAgents
|
|
36
|
+
TAXONOMY_TYPES = %i[knowledge workflow operations monitoring].freeze
|
|
37
|
+
LEGACY_TAXONOMY_TYPES = %i[creation].freeze
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
attr_writer :config
|
|
41
|
+
|
|
42
|
+
def config
|
|
43
|
+
@config ||= Configuration.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def configure
|
|
47
|
+
yield config
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def reset!
|
|
51
|
+
@config = Configuration.new
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# PRD uses RailsAgent - alias to RailsAgents for compatibility
|
|
57
|
+
RailsAgent = RailsAgents unless defined?(RailsAgent)
|
|
@@ -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 = ["Rails Agent Team"]
|
|
9
|
+
spec.email = ["hello@railsagents.dev"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Rails AI agents framework — fullstack Ruby on Rails agents " \
|
|
12
|
+
"(ActiveAgent / RubyLLM alternative)."
|
|
13
|
+
spec.description = "Rails Agent is the most advanced agentic platform for Ruby on Rails. " \
|
|
14
|
+
"Mount /agents, scaffold agent directories, attach BYOK model credentials, " \
|
|
15
|
+
"and build with instructions, tools, connectors, channels, skills, packages, " \
|
|
16
|
+
"knowledge, memory, and guardrails. Sync, run, deploy, and monitor on " \
|
|
17
|
+
"Rails Agent Cloud — a production Rails agent framework and better " \
|
|
18
|
+
"alternative to RubyLLM and ActiveAgent for fullstack Rails AI agents."
|
|
19
|
+
spec.homepage = "https://rails-agent.com"
|
|
20
|
+
spec.license = "MIT"
|
|
21
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
22
|
+
|
|
23
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
24
|
+
spec.metadata["documentation_uri"] = "https://rails-agent.com/docs/getting-started"
|
|
25
|
+
spec.metadata["source_code_uri"] = "https://github.com/Tiny-Bubble-Company/rails-agents"
|
|
26
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/Tiny-Bubble-Company/rails-agents/issues"
|
|
27
|
+
spec.metadata["changelog_uri"] = "https://github.com/Tiny-Bubble-Company/rails-agents/releases"
|
|
28
|
+
spec.metadata["rubygems_mimetype"] = "application/x-rubygem"
|
|
29
|
+
|
|
30
|
+
spec.files = Dir.chdir(__dir__) do
|
|
31
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
32
|
+
f == "Gemfile.lock" || f.start_with?("spec/fixtures/")
|
|
33
|
+
end
|
|
34
|
+
rescue StandardError
|
|
35
|
+
Dir["{app,config,exe,lib,docs}/**/*", "exe/*", "*.{md,gemspec,rake}", "MIT-LICENSE", "AGENTS.md"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
spec.bindir = "exe"
|
|
39
|
+
spec.executables = ["rails-agents"]
|
|
40
|
+
spec.require_paths = ["lib"]
|
|
41
|
+
|
|
42
|
+
spec.add_dependency "railties", ">= 7.0"
|
|
43
|
+
spec.add_dependency "thor", "~> 1.3"
|
|
44
|
+
spec.add_dependency "open-wire", "~> 0.1"
|
|
45
|
+
|
|
46
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
47
|
+
spec.add_development_dependency "rspec", "~> 3.13"
|
|
48
|
+
spec.add_development_dependency "webmock", "~> 3.23"
|
|
49
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"folders": [
|
|
3
|
+
{
|
|
4
|
+
"path": ".",
|
|
5
|
+
"name": "rails-agents"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"path": "../rails-agents-cloud",
|
|
9
|
+
"name": "rails-agents-cloud"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"path": "../rails-agents-boilerplate"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"path": "../rails-agents-ops"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "../rails-agents-design"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"path": "../open-wire",
|
|
22
|
+
"name": "open-wire"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "Agent generator templates" do
|
|
6
|
+
templates_root = File.expand_path("../../lib/generators/rails_agents/agent/templates", __dir__)
|
|
7
|
+
|
|
8
|
+
it "default agent template uses BYOK model DSL and taxonomy base class" do
|
|
9
|
+
content = File.read(File.join(templates_root, "agent.rb.tt"))
|
|
10
|
+
expect(content).to include("<%= agent_base_class %>")
|
|
11
|
+
expect(content).to include("credential: :company_openai")
|
|
12
|
+
expect(content).not_to include("model :auto")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "database template scaffolds KnowledgeAgent with order and product tools" do
|
|
16
|
+
content = File.read(File.join(templates_root, "agent_database.rb.tt"))
|
|
17
|
+
expect(content).to include("KnowledgeAgent")
|
|
18
|
+
expect(content).to include("lookup_order")
|
|
19
|
+
expect(content).to include("search_products")
|
|
20
|
+
expect(content).to include("Order.find_by")
|
|
21
|
+
expect(content).to include("Product.where")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "database prompt documents demo orders and BYOK" do
|
|
25
|
+
content = File.read(File.join(templates_root, "prompt_database.md.tt"))
|
|
26
|
+
expect(content).to include("ORD-DEMO-1001")
|
|
27
|
+
expect(content).to include("BYOK")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "scaffolds an explicit workspace Library imports manifest" do
|
|
31
|
+
content = File.read(File.join(templates_root, "imports.yml.tt"))
|
|
32
|
+
|
|
33
|
+
expect(content).to include("imports: []")
|
|
34
|
+
expect(content).to include("library/skills/triage.rb")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "pathname"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
|
|
8
|
+
RSpec.describe RailsAgents::Base do
|
|
9
|
+
let(:agent_dir) { Pathname.new(Dir.mktmpdir("agent")) }
|
|
10
|
+
|
|
11
|
+
after do
|
|
12
|
+
FileUtils.rm_rf(agent_dir)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
let(:support_agent) do
|
|
16
|
+
Class.new(described_class) do
|
|
17
|
+
model :gpt_5_mini, provider: :openai, credential: :company_openai
|
|
18
|
+
memory :conversation, provider: :mem0, recall: 5
|
|
19
|
+
knowledge_from "knowledge/**/*"
|
|
20
|
+
|
|
21
|
+
tool :lookup_order do |order_id:|
|
|
22
|
+
{ id: order_id, status: "shipped" }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
skill :triage, from: "skills/triage.rb"
|
|
26
|
+
channel :slack
|
|
27
|
+
end.tap do |klass|
|
|
28
|
+
klass.instance_variable_set(:@agent_directory, agent_dir)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
let(:client) { instance_double(RailsAgents::Client) }
|
|
33
|
+
|
|
34
|
+
before do
|
|
35
|
+
FileUtils.mkdir_p(agent_dir.join("knowledge"))
|
|
36
|
+
File.write(agent_dir.join("knowledge/faq.md"), "# FAQ")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "stores DSL configuration on the class" do
|
|
40
|
+
expect(support_agent.model_setting).to eq(:gpt_5_mini)
|
|
41
|
+
expect(support_agent.model_provider).to eq(:openai)
|
|
42
|
+
expect(support_agent.model_credential).to eq(:company_openai)
|
|
43
|
+
expect(support_agent.memory_setting).to eq(:conversation)
|
|
44
|
+
expect(support_agent.memory_provider).to eq(:mem0)
|
|
45
|
+
expect(support_agent.memory_recall).to eq(5)
|
|
46
|
+
expect(support_agent.knowledge_glob).to eq("knowledge/**/*")
|
|
47
|
+
expect(support_agent.tool_definitions.keys).to include(:lookup_order)
|
|
48
|
+
expect(support_agent.skill_definitions[:triage]).to eq("skills/triage.rb")
|
|
49
|
+
expect(support_agent.channel_definitions).to include(:slack)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "supports legacy model :auto without provider metadata" do
|
|
53
|
+
legacy = Class.new(described_class) { model :auto }
|
|
54
|
+
instance = legacy.new(client: client)
|
|
55
|
+
allow(client).to receive(:create_run).and_return({ "id" => "run_1" })
|
|
56
|
+
|
|
57
|
+
legacy.new(client: client).run("Hi")
|
|
58
|
+
|
|
59
|
+
expect(client).to have_received(:create_run).with(
|
|
60
|
+
hash_including(metadata: hash_including(model: :auto, kind: :base))
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "supports keyword trigger inputs like order_id:" do
|
|
65
|
+
allow(client).to receive(:create_run).and_return({ "id" => "run_kw" })
|
|
66
|
+
|
|
67
|
+
support_agent.run(order_id: 1, client: client)
|
|
68
|
+
|
|
69
|
+
expect(client).to have_received(:create_run).with(
|
|
70
|
+
hash_including(message: "order_id: 1")
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "rejects mixing a message string with keyword inputs" do
|
|
75
|
+
expect {
|
|
76
|
+
support_agent.run("hello", order_id: 1, client: client)
|
|
77
|
+
}.to raise_error(ArgumentError, /either a message string or keyword/)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "executes tool blocks" do
|
|
81
|
+
result = support_agent.tool_definitions[:lookup_order].call(order_id: 42)
|
|
82
|
+
expect(result[:status]).to eq("shipped")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "collects knowledge paths relative to agent directory" do
|
|
86
|
+
agent = support_agent.new(client: client)
|
|
87
|
+
expect(agent.knowledge_paths).to eq(["knowledge/faq.md"])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "includes kind and structured model in run metadata" do
|
|
91
|
+
allow(client).to receive(:create_run).and_return({ "id" => "run_abc" })
|
|
92
|
+
|
|
93
|
+
support_agent.new(client: client).run("Hello", session_id: "sess_1")
|
|
94
|
+
|
|
95
|
+
expect(client).to have_received(:create_run).with(
|
|
96
|
+
hash_including(
|
|
97
|
+
agent: agent_dir.basename.to_s,
|
|
98
|
+
message: "Hello",
|
|
99
|
+
session_id: "sess_1",
|
|
100
|
+
metadata: hash_including(
|
|
101
|
+
kind: :base,
|
|
102
|
+
model: {
|
|
103
|
+
name: :gpt_5_mini,
|
|
104
|
+
provider: :openai,
|
|
105
|
+
credential: :company_openai
|
|
106
|
+
},
|
|
107
|
+
memory: {
|
|
108
|
+
type: :conversation,
|
|
109
|
+
provider: :mem0,
|
|
110
|
+
recall: 5
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it "creates a run via the cloud client" do
|
|
118
|
+
allow(client).to receive(:create_run).and_return({ "id" => "run_abc" })
|
|
119
|
+
|
|
120
|
+
result = support_agent.new(client: client).run("Hello", session_id: "sess_1")
|
|
121
|
+
|
|
122
|
+
expect(result.run_id).to eq("run_abc")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "deploys via the cloud client" do
|
|
126
|
+
allow(client).to receive(:deploy).and_return({ "id" => "dep_1" })
|
|
127
|
+
|
|
128
|
+
response = support_agent.new(client: client).deploy
|
|
129
|
+
|
|
130
|
+
expect(client).to have_received(:deploy).with(
|
|
131
|
+
agent: agent_dir.basename.to_s,
|
|
132
|
+
bundle_path: agent_dir.to_s
|
|
133
|
+
)
|
|
134
|
+
expect(response["id"]).to eq("dep_1")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "syncs knowledge via the cloud client" do
|
|
138
|
+
allow(client).to receive(:sync_knowledge).and_return({ "synced" => 1 })
|
|
139
|
+
|
|
140
|
+
support_agent.new(client: client).sync_knowledge
|
|
141
|
+
|
|
142
|
+
expect(client).to have_received(:sync_knowledge).with(
|
|
143
|
+
agent: agent_dir.basename.to_s,
|
|
144
|
+
paths: ["knowledge/faq.md"]
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "streams run output" do
|
|
149
|
+
allow(client).to receive(:create_run).and_return({ "id" => "run_1" })
|
|
150
|
+
allow(client).to receive(:stream_run).and_yield({ "content" => "Hi" }).and_yield({ "content" => " there" })
|
|
151
|
+
|
|
152
|
+
result = support_agent.run("Hello", client: client)
|
|
153
|
+
expect(result.output).to eq("Hi there")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it "prefers done output_text over token chunks when draining stream" do
|
|
157
|
+
allow(client).to receive(:create_run).and_return({ "id" => "run_3" })
|
|
158
|
+
allow(client).to receive(:stream_run)
|
|
159
|
+
.and_yield({ "content" => "partial" })
|
|
160
|
+
.and_yield({
|
|
161
|
+
"output_text" => "Final markdown",
|
|
162
|
+
"output" => "Final markdown",
|
|
163
|
+
"format" => "markdown",
|
|
164
|
+
"items" => [],
|
|
165
|
+
"status" => "succeeded"
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
result = support_agent.run("Hello", client: client)
|
|
169
|
+
expect(result.output).to eq("Final markdown")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it "exposes Eve-aligned output_text / items from sync create_run" do
|
|
173
|
+
allow(client).to receive(:create_run).and_return({
|
|
174
|
+
"object" => "run",
|
|
175
|
+
"data" => {
|
|
176
|
+
"id" => "run_2",
|
|
177
|
+
"output" => "| Name | Qty |\n| --- | --- |\n| Tee | 2 |",
|
|
178
|
+
"output_text" => "| Name | Qty |\n| --- | --- |\n| Tee | 2 |",
|
|
179
|
+
"output_data" => nil,
|
|
180
|
+
"items" => [
|
|
181
|
+
{
|
|
182
|
+
"type" => "message",
|
|
183
|
+
"role" => "assistant",
|
|
184
|
+
"content" => "| Name | Qty |\n| --- | --- |\n| Tee | 2 |",
|
|
185
|
+
"format" => "markdown"
|
|
186
|
+
}
|
|
187
|
+
],
|
|
188
|
+
"format" => "markdown",
|
|
189
|
+
"status" => "succeeded"
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
result = support_agent.run("List products", client: client)
|
|
194
|
+
expect(result.run_id).to eq("run_2")
|
|
195
|
+
expect(result.output_text).to include("Tee")
|
|
196
|
+
expect(result.output).to eq(result.output_text)
|
|
197
|
+
expect(result.format).to eq("markdown")
|
|
198
|
+
expect(result.items.first["type"]).to eq("message")
|
|
199
|
+
expect(result.output_data).to be_nil
|
|
200
|
+
expect(result.status).to eq("succeeded")
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe RailsAgents::Client do
|
|
6
|
+
let(:config) do
|
|
7
|
+
RailsAgents.config.tap do |c|
|
|
8
|
+
c.api_key = "test-key"
|
|
9
|
+
c.api_base = "https://cloud.rails-agent.com"
|
|
10
|
+
c.project_id = "proj_123"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
subject(:client) { described_class.new(config: config) }
|
|
15
|
+
|
|
16
|
+
describe "#create_run" do
|
|
17
|
+
it "POSTs to /api/v1/runs with auth" do
|
|
18
|
+
stub_request(:post, "https://cloud.rails-agent.com/api/v1/runs")
|
|
19
|
+
.with(
|
|
20
|
+
headers: { "Authorization" => "Bearer test-key" },
|
|
21
|
+
body: hash_including("agent" => "support", "message" => "Hello")
|
|
22
|
+
)
|
|
23
|
+
.to_return(status: 200, body: { id: "run_1" }.to_json, headers: { "Content-Type" => "application/json" })
|
|
24
|
+
|
|
25
|
+
response = client.create_run(agent: "support", message: "Hello")
|
|
26
|
+
expect(response["id"]).to eq("run_1")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "extracts nested API error messages" do
|
|
30
|
+
stub_request(:post, "https://cloud.rails-agent.com/api/v1/runs")
|
|
31
|
+
.to_return(
|
|
32
|
+
status: 401,
|
|
33
|
+
body: { error: { message: "Invalid API key" } }.to_json,
|
|
34
|
+
headers: { "Content-Type" => "application/json" }
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
expect do
|
|
38
|
+
client.create_run(agent: "support", message: "Hello")
|
|
39
|
+
end.to raise_error(RailsAgents::Client::Error, "Invalid API key")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe "#deploy" do
|
|
44
|
+
it "POSTs to /api/v1/deploys" do
|
|
45
|
+
stub_request(:post, "https://cloud.rails-agent.com/api/v1/deploys")
|
|
46
|
+
.to_return(status: 200, body: { id: "dep_1", status: "queued" }.to_json)
|
|
47
|
+
|
|
48
|
+
response = client.deploy(agent: "support")
|
|
49
|
+
expect(response["status"]).to eq("queued")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe "#install_channel" do
|
|
54
|
+
it "POSTs to /api/v1/channels/:kind/install" do
|
|
55
|
+
stub_request(:post, "https://cloud.rails-agent.com/api/v1/channels/slack/install")
|
|
56
|
+
.to_return(status: 200, body: { url: "https://oauth.example" }.to_json)
|
|
57
|
+
|
|
58
|
+
response = client.install_channel(kind: "slack")
|
|
59
|
+
expect(response["url"]).to include("oauth")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe "#sync_knowledge" do
|
|
64
|
+
it "POSTs to /api/v1/knowledge/sync" do
|
|
65
|
+
stub_request(:post, "https://cloud.rails-agent.com/api/v1/knowledge/sync")
|
|
66
|
+
.to_return(status: 200, body: { synced: 2 }.to_json)
|
|
67
|
+
|
|
68
|
+
response = client.sync_knowledge(agent: "support", paths: %w[faq.md])
|
|
69
|
+
expect(response["synced"]).to eq(2)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe "#sync_files" do
|
|
74
|
+
it "PUTs to /api/v1/agents/:id/files" do
|
|
75
|
+
stub_request(:put, "https://cloud.rails-agent.com/api/v1/agents/support/files")
|
|
76
|
+
.with(body: hash_including("files" => kind_of(Array)))
|
|
77
|
+
.to_return(status: 200, body: { data: [] }.to_json)
|
|
78
|
+
|
|
79
|
+
response = client.sync_files(agent: "support", files: [{ "path" => "app/agents/support/agent.rb", "content" => "class Support; end" }])
|
|
80
|
+
expect(response["data"]).to eq([])
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe "#handshake" do
|
|
85
|
+
it "POSTs to /api/v1/auth/handshake without bearer" do
|
|
86
|
+
stub_request(:post, "https://cloud.rails-agent.com/api/v1/auth/handshake")
|
|
87
|
+
.with { |req| !req.headers["Authorization"] }
|
|
88
|
+
.to_return(status: 201, body: { data: { api_key: "ra_x" } }.to_json)
|
|
89
|
+
|
|
90
|
+
response = client.handshake(email: "a@b.com", password: "password1")
|
|
91
|
+
expect(response.dig("data", "api_key")).to eq("ra_x")
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe "#logs" do
|
|
96
|
+
it "GETs /api/v1/logs" do
|
|
97
|
+
stub_request(:get, %r{https://cloud.rails-agent.com/api/v1/logs})
|
|
98
|
+
.to_return(status: 200, body: { entries: [] }.to_json)
|
|
99
|
+
|
|
100
|
+
expect(client.logs(agent: "support")["entries"]).to eq([])
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
describe "#traces" do
|
|
105
|
+
it "GETs /api/v1/traces" do
|
|
106
|
+
stub_request(:get, %r{https://cloud.rails-agent.com/api/v1/traces})
|
|
107
|
+
.to_return(status: 200, body: { traces: [] }.to_json)
|
|
108
|
+
|
|
109
|
+
expect(client.traces["traces"]).to eq([])
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
describe "#evals" do
|
|
114
|
+
it "GETs /api/v1/evals" do
|
|
115
|
+
stub_request(:get, %r{https://cloud.rails-agent.com/api/v1/evals})
|
|
116
|
+
.to_return(status: 200, body: { evals: [] }.to_json)
|
|
117
|
+
|
|
118
|
+
expect(client.evals["evals"]).to eq([])
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe RailsAgents::Configuration do
|
|
6
|
+
subject(:config) { described_class.new }
|
|
7
|
+
|
|
8
|
+
it "defaults api_base to cloud.rails-agent.com" do
|
|
9
|
+
expect(config.api_base).to eq("https://cloud.rails-agent.com")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "defaults dashboard_base to cloud.rails-agent.com" do
|
|
13
|
+
expect(config.dashboard_base).to eq("https://cloud.rails-agent.com")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "builds api_v1_base under /api/v1" do
|
|
17
|
+
expect(config.api_v1_base).to eq("https://cloud.rails-agent.com/api/v1")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "reads api_key from ENV" do
|
|
21
|
+
with_env("RAILS_AGENTS_API_KEY" => "test-key") do
|
|
22
|
+
expect(described_class.new.api_key).to eq("test-key")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "reads project_id from ENV" do
|
|
27
|
+
with_env("RAILS_AGENTS_PROJECT_ID" => "prj_test") do
|
|
28
|
+
expect(described_class.new.project_id).to eq("prj_test")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "ignores RAILS_AGENTS_API_BASE env (cloud host is fixed)" do
|
|
33
|
+
with_env("RAILS_AGENTS_API_BASE" => "https://example.invalid") do
|
|
34
|
+
expect(described_class.new.api_base).to eq("https://cloud.rails-agent.com")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "reports configured when api_key is present" do
|
|
39
|
+
config.api_key = "abc"
|
|
40
|
+
expect(config).to be_configured
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "reports not configured when api_key is blank" do
|
|
44
|
+
config.api_key = ""
|
|
45
|
+
expect(config).not_to be_configured
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def with_env(vars)
|
|
49
|
+
old = vars.keys.to_h { |k| [k, ENV[k]] }
|
|
50
|
+
vars.each { |k, v| ENV[k] = v }
|
|
51
|
+
yield
|
|
52
|
+
ensure
|
|
53
|
+
old.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v }
|
|
54
|
+
end
|
|
55
|
+
end
|