ruby_llm-agents 3.7.2 → 3.9.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 +4 -4
- data/README.md +30 -10
- data/app/controllers/ruby_llm/agents/agents_controller.rb +14 -141
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +12 -166
- data/app/controllers/ruby_llm/agents/executions_controller.rb +1 -1
- data/app/controllers/ruby_llm/agents/requests_controller.rb +117 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +38 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +302 -103
- data/app/models/ruby_llm/agents/execution.rb +76 -54
- data/app/models/ruby_llm/agents/execution_detail.rb +2 -0
- data/app/models/ruby_llm/agents/tenant.rb +39 -0
- data/app/services/ruby_llm/agents/agent_registry.rb +98 -0
- data/app/views/layouts/ruby_llm/agents/application.html.erb +4 -2
- data/app/views/ruby_llm/agents/executions/_list.html.erb +3 -17
- data/app/views/ruby_llm/agents/requests/index.html.erb +153 -0
- data/app/views/ruby_llm/agents/requests/show.html.erb +136 -0
- data/config/routes.rb +2 -0
- data/lib/generators/ruby_llm_agents/agent_generator.rb +2 -2
- data/lib/generators/ruby_llm_agents/demo_generator.rb +102 -0
- data/lib/generators/ruby_llm_agents/doctor_generator.rb +196 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +7 -19
- data/lib/generators/ruby_llm_agents/templates/add_dashboard_performance_indexes_migration.rb.tt +23 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +27 -80
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +18 -51
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +19 -17
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +3 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +25 -0
- data/lib/ruby_llm/agents/base_agent.rb +71 -4
- data/lib/ruby_llm/agents/core/base.rb +4 -0
- data/lib/ruby_llm/agents/core/configuration.rb +11 -0
- data/lib/ruby_llm/agents/core/instrumentation.rb +15 -19
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +4 -4
- data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +19 -11
- data/lib/ruby_llm/agents/pipeline/builder.rb +8 -4
- data/lib/ruby_llm/agents/pipeline/context.rb +69 -1
- data/lib/ruby_llm/agents/pipeline/middleware/base.rb +58 -4
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +21 -17
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +40 -26
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +126 -120
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +13 -11
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +29 -31
- data/lib/ruby_llm/agents/providers/inception/capabilities.rb +107 -0
- data/lib/ruby_llm/agents/providers/inception/chat.rb +17 -0
- data/lib/ruby_llm/agents/providers/inception/configuration.rb +9 -0
- data/lib/ruby_llm/agents/providers/inception/models.rb +38 -0
- data/lib/ruby_llm/agents/providers/inception/registry.rb +45 -0
- data/lib/ruby_llm/agents/providers/inception.rb +50 -0
- data/lib/ruby_llm/agents/rails/engine.rb +11 -0
- data/lib/ruby_llm/agents/results/background_removal_result.rb +7 -1
- data/lib/ruby_llm/agents/results/base.rb +28 -4
- data/lib/ruby_llm/agents/results/embedding_result.rb +4 -0
- data/lib/ruby_llm/agents/results/image_analysis_result.rb +11 -3
- data/lib/ruby_llm/agents/results/image_edit_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_generation_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_pipeline_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_transform_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_upscale_result.rb +7 -1
- data/lib/ruby_llm/agents/results/image_variation_result.rb +7 -1
- data/lib/ruby_llm/agents/results/speech_result.rb +6 -0
- data/lib/ruby_llm/agents/results/trackable.rb +25 -0
- data/lib/ruby_llm/agents/results/transcription_result.rb +6 -0
- data/lib/ruby_llm/agents/text/embedder.rb +8 -1
- data/lib/ruby_llm/agents/track_report.rb +127 -0
- data/lib/ruby_llm/agents/tracker.rb +32 -0
- data/lib/ruby_llm/agents.rb +212 -0
- data/lib/tasks/ruby_llm_agents.rake +6 -0
- metadata +17 -2
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<div class="flex items-center gap-3 mb-6">
|
|
2
|
+
<%= link_to ruby_llm_agents.requests_path, class: "text-gray-400 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-300 transition-colors" do %>
|
|
3
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
4
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"/>
|
|
5
|
+
</svg>
|
|
6
|
+
<% end %>
|
|
7
|
+
<span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">request</span>
|
|
8
|
+
<code class="font-mono text-xs text-gray-900 dark:text-gray-200 bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded"><%= @request_id %></code>
|
|
9
|
+
<div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
|
|
10
|
+
<% if @summary[:all_successful] %>
|
|
11
|
+
<span class="badge badge-sm badge-success">all ok</span>
|
|
12
|
+
<% elsif @summary[:error_count] > 0 %>
|
|
13
|
+
<span class="badge badge-sm badge-error"><%= @summary[:error_count] %> error<%= @summary[:error_count] > 1 ? "s" : "" %></span>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<!-- Summary Stats -->
|
|
18
|
+
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
|
|
19
|
+
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-lg px-4 py-3">
|
|
20
|
+
<div class="font-mono text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider mb-1">calls</div>
|
|
21
|
+
<div class="font-mono text-lg text-gray-900 dark:text-gray-100"><%= @summary[:call_count] %></div>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-lg px-4 py-3">
|
|
24
|
+
<div class="font-mono text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider mb-1">total cost</div>
|
|
25
|
+
<div class="font-mono text-lg text-gray-900 dark:text-gray-100">$<%= number_with_precision(@summary[:total_cost] || 0, precision: 4) %></div>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-lg px-4 py-3">
|
|
28
|
+
<div class="font-mono text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider mb-1">tokens</div>
|
|
29
|
+
<div class="font-mono text-lg text-gray-900 dark:text-gray-100"><%= number_with_delimiter(@summary[:total_tokens] || 0) %></div>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-lg px-4 py-3">
|
|
32
|
+
<div class="font-mono text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider mb-1">duration</div>
|
|
33
|
+
<div class="font-mono text-lg text-gray-900 dark:text-gray-100">
|
|
34
|
+
<%= @summary[:duration_ms] ? format_duration_ms(@summary[:duration_ms]) : "—" %>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<!-- Agents & Models -->
|
|
40
|
+
<div class="flex flex-wrap gap-2 mb-6">
|
|
41
|
+
<% @summary[:agent_types].each do |agent_type| %>
|
|
42
|
+
<%= link_to agent_type.gsub(/Agent$/, ""),
|
|
43
|
+
ruby_llm_agents.agent_path(agent_type),
|
|
44
|
+
class: "font-mono text-xs px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors" %>
|
|
45
|
+
<% end %>
|
|
46
|
+
<span class="w-px h-4 bg-gray-200 dark:bg-gray-700 self-center"></span>
|
|
47
|
+
<% @summary[:models_used].each do |model| %>
|
|
48
|
+
<span class="font-mono text-xs px-2 py-0.5 rounded bg-gray-50 dark:bg-gray-800/30 text-gray-400 dark:text-gray-600"><%= model %></span>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!-- Execution Timeline -->
|
|
53
|
+
<div class="mb-2">
|
|
54
|
+
<span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">execution timeline</span>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="space-y-px">
|
|
58
|
+
<!-- Column headers -->
|
|
59
|
+
<div class="flex items-center gap-3 px-2 -mx-2 font-mono text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider mb-1">
|
|
60
|
+
<span class="w-5 flex-shrink-0 text-center">#</span>
|
|
61
|
+
<span class="flex-1 min-w-0">agent</span>
|
|
62
|
+
<span class="w-20 flex-shrink-0">status</span>
|
|
63
|
+
<span class="flex-1 min-w-0 hidden lg:block">model</span>
|
|
64
|
+
<span class="w-16 flex-shrink-0 text-right">duration</span>
|
|
65
|
+
<span class="w-16 flex-shrink-0 text-right hidden md:block">tokens</span>
|
|
66
|
+
<span class="w-16 flex-shrink-0 text-right hidden md:block">cost</span>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div class="font-mono text-xs">
|
|
70
|
+
<% @executions.each_with_index do |execution, idx| %>
|
|
71
|
+
<%
|
|
72
|
+
status_badge_class = case execution.status.to_s
|
|
73
|
+
when "running" then "badge-running"
|
|
74
|
+
when "success" then "badge-success"
|
|
75
|
+
when "error" then "badge-error"
|
|
76
|
+
when "timeout" then "badge-timeout"
|
|
77
|
+
else "badge-timeout"
|
|
78
|
+
end
|
|
79
|
+
%>
|
|
80
|
+
<div class="group flex items-center gap-3 py-1.5 px-2 -mx-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800/50 cursor-pointer"
|
|
81
|
+
onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'">
|
|
82
|
+
<span class="w-5 flex-shrink-0 text-center text-gray-300 dark:text-gray-700"><%= idx + 1 %></span>
|
|
83
|
+
<span class="flex-1 min-w-0 truncate">
|
|
84
|
+
<%= link_to execution.agent_type.gsub(/Agent$/, ""),
|
|
85
|
+
ruby_llm_agents.agent_path(execution.agent_type),
|
|
86
|
+
class: "text-gray-900 dark:text-gray-200 hover:underline",
|
|
87
|
+
onclick: "event.stopPropagation()" %>
|
|
88
|
+
</span>
|
|
89
|
+
<span class="w-20 flex-shrink-0">
|
|
90
|
+
<span class="badge badge-sm <%= status_badge_class %>"><%= execution.status %></span>
|
|
91
|
+
</span>
|
|
92
|
+
<span class="flex-1 min-w-0 truncate text-gray-400 dark:text-gray-600 hidden lg:inline" title="<%= execution.model_id %>"><%= execution.model_id %></span>
|
|
93
|
+
<span class="w-16 flex-shrink-0 text-right text-gray-500 dark:text-gray-400">
|
|
94
|
+
<% if execution.status_running? %>
|
|
95
|
+
<span class="text-blue-500 animate-pulse">...</span>
|
|
96
|
+
<% else %>
|
|
97
|
+
<%= execution.duration_ms ? format_duration_ms(execution.duration_ms) : "—" %>
|
|
98
|
+
<% end %>
|
|
99
|
+
</span>
|
|
100
|
+
<span class="w-16 flex-shrink-0 text-right text-gray-500 dark:text-gray-400 hidden md:inline"><%= number_with_delimiter(execution.total_tokens || 0) %></span>
|
|
101
|
+
<span class="w-16 flex-shrink-0 text-right text-gray-500 dark:text-gray-400 hidden md:inline">$<%= number_with_precision(execution.total_cost || 0, precision: 4) %></span>
|
|
102
|
+
</div>
|
|
103
|
+
<% if execution.status_error? && execution.error_class.present? %>
|
|
104
|
+
<div class="flex items-center gap-1 pl-8 py-0.5 text-red-400 dark:text-red-500/70 text-xs font-mono">
|
|
105
|
+
<span class="text-gray-300 dark:text-gray-700">└</span>
|
|
106
|
+
<span class="truncate"><%= execution.error_class.split("::").last %><%= execution.error_message.present? ? ": #{truncate(execution.error_message, length: 100)}" : "" %></span>
|
|
107
|
+
</div>
|
|
108
|
+
<% end %>
|
|
109
|
+
<% end %>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<!-- Timing Details -->
|
|
114
|
+
<% if @summary[:started_at] %>
|
|
115
|
+
<div class="mt-6 pt-4 border-t border-gray-200 dark:border-gray-800">
|
|
116
|
+
<span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">timing</span>
|
|
117
|
+
<div class="mt-2 font-mono text-xs text-gray-500 dark:text-gray-400 space-y-1">
|
|
118
|
+
<div class="flex gap-4">
|
|
119
|
+
<span class="text-gray-400 dark:text-gray-600 w-20">started</span>
|
|
120
|
+
<span><%= @summary[:started_at].strftime("%Y-%m-%d %H:%M:%S.%L") %></span>
|
|
121
|
+
</div>
|
|
122
|
+
<% if @summary[:completed_at] %>
|
|
123
|
+
<div class="flex gap-4">
|
|
124
|
+
<span class="text-gray-400 dark:text-gray-600 w-20">completed</span>
|
|
125
|
+
<span><%= @summary[:completed_at].strftime("%Y-%m-%d %H:%M:%S.%L") %></span>
|
|
126
|
+
</div>
|
|
127
|
+
<% end %>
|
|
128
|
+
<% if @summary[:duration_ms] %>
|
|
129
|
+
<div class="flex gap-4">
|
|
130
|
+
<span class="text-gray-400 dark:text-gray-600 w-20">wall clock</span>
|
|
131
|
+
<span><%= format_duration_ms(@summary[:duration_ms]) %></span>
|
|
132
|
+
</div>
|
|
133
|
+
<% end %>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -21,8 +21,8 @@ module RubyLlmAgents
|
|
|
21
21
|
|
|
22
22
|
argument :params, type: :array, default: [], banner: "param[:required|:default] param[:required|:default]"
|
|
23
23
|
|
|
24
|
-
class_option :model, type: :string, default: "
|
|
25
|
-
desc: "The LLM model to use"
|
|
24
|
+
class_option :model, type: :string, default: "default",
|
|
25
|
+
desc: "The LLM model to use (omit to use configured default)"
|
|
26
26
|
class_option :temperature, type: :numeric, default: 0.0,
|
|
27
27
|
desc: "The temperature setting (0.0-1.0)"
|
|
28
28
|
class_option :cache, type: :string, default: nil,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module RubyLlmAgents
|
|
6
|
+
# Demo generator — scaffolds a working HelloAgent with a smoke-test script.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# rails generate ruby_llm_agents:demo
|
|
10
|
+
#
|
|
11
|
+
# Creates:
|
|
12
|
+
# - app/agents/hello_agent.rb — minimal working agent
|
|
13
|
+
# - bin/smoke_test_agent — one-command verification script
|
|
14
|
+
#
|
|
15
|
+
class DemoGenerator < ::Rails::Generators::Base
|
|
16
|
+
source_root File.expand_path("templates", __dir__)
|
|
17
|
+
|
|
18
|
+
def ensure_base_class
|
|
19
|
+
agents_dir = "app/agents"
|
|
20
|
+
empty_directory agents_dir
|
|
21
|
+
|
|
22
|
+
base_class_path = "#{agents_dir}/application_agent.rb"
|
|
23
|
+
unless File.exist?(File.join(destination_root, base_class_path))
|
|
24
|
+
template "application_agent.rb.tt", base_class_path
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def create_hello_agent
|
|
29
|
+
create_file "app/agents/hello_agent.rb", <<~RUBY
|
|
30
|
+
# frozen_string_literal: true
|
|
31
|
+
|
|
32
|
+
class HelloAgent < ApplicationAgent
|
|
33
|
+
system "You are a friendly assistant. Keep responses under 2 sentences."
|
|
34
|
+
|
|
35
|
+
prompt "Say hello to {name} and tell them one fun fact."
|
|
36
|
+
end
|
|
37
|
+
RUBY
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def create_smoke_test
|
|
41
|
+
create_file "bin/smoke_test_agent", <<~RUBY
|
|
42
|
+
#!/usr/bin/env ruby
|
|
43
|
+
# frozen_string_literal: true
|
|
44
|
+
|
|
45
|
+
# Smoke test — verifies your RubyLLM::Agents setup end-to-end.
|
|
46
|
+
#
|
|
47
|
+
# Usage:
|
|
48
|
+
# bin/rails runner bin/smoke_test_agent
|
|
49
|
+
#
|
|
50
|
+
puts "Running RubyLLM::Agents smoke test..."
|
|
51
|
+
puts ""
|
|
52
|
+
|
|
53
|
+
# 1. Check configuration
|
|
54
|
+
config = RubyLLM::Agents.configuration
|
|
55
|
+
model = config.default_model
|
|
56
|
+
puts "Default model: \#{model}"
|
|
57
|
+
|
|
58
|
+
# 2. Dry-run (no API call)
|
|
59
|
+
puts ""
|
|
60
|
+
puts "Dry run:"
|
|
61
|
+
dry = HelloAgent.call(name: "World", dry_run: true)
|
|
62
|
+
puts " System prompt: \#{dry.system_prompt[0..80]}..."
|
|
63
|
+
puts " User prompt: \#{dry.user_prompt}"
|
|
64
|
+
puts " Model: \#{dry.model}"
|
|
65
|
+
puts " Dry run OK!"
|
|
66
|
+
|
|
67
|
+
# 3. Live call
|
|
68
|
+
puts ""
|
|
69
|
+
puts "Live call (calling \#{model})..."
|
|
70
|
+
begin
|
|
71
|
+
result = HelloAgent.call(name: "World")
|
|
72
|
+
puts " Response: \#{result.content}"
|
|
73
|
+
puts ""
|
|
74
|
+
puts "Success! Your setup is working."
|
|
75
|
+
rescue => e
|
|
76
|
+
puts " Error: \#{e.class}: \#{e.message}"
|
|
77
|
+
puts ""
|
|
78
|
+
puts "The dry run worked but the live call failed."
|
|
79
|
+
puts "This usually means your API key is missing or invalid."
|
|
80
|
+
puts ""
|
|
81
|
+
puts "Run 'rails ruby_llm_agents:doctor' for detailed diagnostics."
|
|
82
|
+
exit 1
|
|
83
|
+
end
|
|
84
|
+
RUBY
|
|
85
|
+
|
|
86
|
+
chmod "bin/smoke_test_agent", 0o755
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def show_next_steps
|
|
90
|
+
say ""
|
|
91
|
+
say "Demo agent created!", :green
|
|
92
|
+
say ""
|
|
93
|
+
say "Try it:"
|
|
94
|
+
say " bin/rails runner bin/smoke_test_agent"
|
|
95
|
+
say ""
|
|
96
|
+
say "Or in the Rails console:"
|
|
97
|
+
say " HelloAgent.call(name: \"World\")"
|
|
98
|
+
say " HelloAgent.call(name: \"World\", dry_run: true)"
|
|
99
|
+
say ""
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module RubyLlmAgents
|
|
6
|
+
# Doctor generator — validates that setup is complete and working.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# rails generate ruby_llm_agents:doctor
|
|
10
|
+
# rails ruby_llm_agents:doctor (rake task alias)
|
|
11
|
+
#
|
|
12
|
+
# Checks:
|
|
13
|
+
# 1. API keys — at least one provider key is configured
|
|
14
|
+
# 2. Migrations — required tables exist
|
|
15
|
+
# 3. Routes — engine is mounted
|
|
16
|
+
# 4. Background jobs — ActiveJob adapter is configured (not :async/:inline in prod)
|
|
17
|
+
# 5. Agents — at least one agent file exists
|
|
18
|
+
#
|
|
19
|
+
class DoctorGenerator < ::Rails::Generators::Base
|
|
20
|
+
desc "Validate your RubyLLM::Agents setup and print actionable fixes"
|
|
21
|
+
|
|
22
|
+
def run_checks
|
|
23
|
+
@pass = 0
|
|
24
|
+
@fail = 0
|
|
25
|
+
@warn = 0
|
|
26
|
+
|
|
27
|
+
say ""
|
|
28
|
+
say "RubyLLM::Agents Doctor", :bold
|
|
29
|
+
say "=" * 40
|
|
30
|
+
|
|
31
|
+
check_api_keys
|
|
32
|
+
check_migrations
|
|
33
|
+
check_routes
|
|
34
|
+
check_background_jobs
|
|
35
|
+
check_agents
|
|
36
|
+
|
|
37
|
+
say ""
|
|
38
|
+
say "=" * 40
|
|
39
|
+
summary = "#{@pass} passed, #{@fail} failed, #{@warn} warnings"
|
|
40
|
+
if @fail > 0
|
|
41
|
+
say "Result: #{summary}", :red
|
|
42
|
+
elsif @warn > 0
|
|
43
|
+
say "Result: #{summary}", :yellow
|
|
44
|
+
else
|
|
45
|
+
say "Result: #{summary} — you're all set!", :green
|
|
46
|
+
end
|
|
47
|
+
say ""
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def check_api_keys
|
|
53
|
+
say ""
|
|
54
|
+
say "API Keys", :bold
|
|
55
|
+
|
|
56
|
+
config = RubyLLM::Agents.configuration
|
|
57
|
+
providers = {
|
|
58
|
+
"OpenAI" => -> { config.openai_api_key },
|
|
59
|
+
"Anthropic" => -> { config.anthropic_api_key },
|
|
60
|
+
"Gemini" => -> { config.gemini_api_key },
|
|
61
|
+
"DeepSeek" => -> { config.deepseek_api_key },
|
|
62
|
+
"OpenRouter" => -> { config.openrouter_api_key },
|
|
63
|
+
"Mistral" => -> { config.mistral_api_key }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
configured = providers.select { |_, v| v.call.present? }.keys
|
|
67
|
+
|
|
68
|
+
if configured.any?
|
|
69
|
+
configured.each { |name| pass "#{name} API key configured" }
|
|
70
|
+
else
|
|
71
|
+
fail_check "No API keys configured"
|
|
72
|
+
fix "Add to config/initializers/ruby_llm_agents.rb:"
|
|
73
|
+
fix " config.openai_api_key = ENV[\"OPENAI_API_KEY\"]"
|
|
74
|
+
fix "Then set the environment variable in .env or credentials."
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def check_migrations
|
|
79
|
+
say ""
|
|
80
|
+
say "Database", :bold
|
|
81
|
+
|
|
82
|
+
tables = {
|
|
83
|
+
"ruby_llm_agents_executions" => "rails generate ruby_llm_agents:install && rails db:migrate",
|
|
84
|
+
"ruby_llm_agents_execution_details" => "rails generate ruby_llm_agents:upgrade && rails db:migrate"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
tables.each do |table, fix_cmd|
|
|
88
|
+
if table_exists?(table)
|
|
89
|
+
pass "Table #{table} exists"
|
|
90
|
+
else
|
|
91
|
+
fail_check "Table #{table} missing"
|
|
92
|
+
fix fix_cmd
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def check_routes
|
|
98
|
+
say ""
|
|
99
|
+
say "Routes", :bold
|
|
100
|
+
|
|
101
|
+
routes_file = File.join(destination_root, "config/routes.rb")
|
|
102
|
+
if File.exist?(routes_file)
|
|
103
|
+
content = File.read(routes_file)
|
|
104
|
+
if content.include?("RubyLLM::Agents::Engine")
|
|
105
|
+
pass "Dashboard engine mounted"
|
|
106
|
+
else
|
|
107
|
+
warn_check "Dashboard engine not mounted in routes"
|
|
108
|
+
fix "Add to config/routes.rb:"
|
|
109
|
+
fix " mount RubyLLM::Agents::Engine => \"/agents\""
|
|
110
|
+
end
|
|
111
|
+
else
|
|
112
|
+
warn_check "Could not find config/routes.rb"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def check_background_jobs
|
|
117
|
+
say ""
|
|
118
|
+
say "Background Jobs", :bold
|
|
119
|
+
|
|
120
|
+
adapter = ActiveJob::Base.queue_adapter.class.name
|
|
121
|
+
async_logging = RubyLLM::Agents.configuration.async_logging
|
|
122
|
+
|
|
123
|
+
if !async_logging
|
|
124
|
+
pass "Async logging disabled (synchronous mode)"
|
|
125
|
+
elsif adapter.include?("Async") || adapter.include?("Inline")
|
|
126
|
+
if Rails.env.production?
|
|
127
|
+
warn_check "ActiveJob adapter is #{adapter} — execution logging may be lost in production"
|
|
128
|
+
fix "Configure a persistent adapter (Sidekiq, GoodJob, SolidQueue, etc.)"
|
|
129
|
+
fix "Or set config.async_logging = false for synchronous logging."
|
|
130
|
+
else
|
|
131
|
+
pass "ActiveJob adapter: #{adapter} (OK for development)"
|
|
132
|
+
end
|
|
133
|
+
else
|
|
134
|
+
pass "ActiveJob adapter: #{adapter}"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def check_agents
|
|
139
|
+
say ""
|
|
140
|
+
say "Agents", :bold
|
|
141
|
+
|
|
142
|
+
agents_dir = File.join(destination_root, "app/agents")
|
|
143
|
+
if Dir.exist?(agents_dir)
|
|
144
|
+
agent_files = Dir.glob(File.join(agents_dir, "**/*_agent.rb"))
|
|
145
|
+
.reject { |f| f.end_with?("application_agent.rb") }
|
|
146
|
+
|
|
147
|
+
if agent_files.any?
|
|
148
|
+
pass "Found #{agent_files.size} agent(s)"
|
|
149
|
+
else
|
|
150
|
+
warn_check "No agents found (only application_agent.rb)"
|
|
151
|
+
fix "rails generate ruby_llm_agents:agent HelloWorld query:required"
|
|
152
|
+
fix "Or: rails generate ruby_llm_agents:demo"
|
|
153
|
+
end
|
|
154
|
+
else
|
|
155
|
+
fail_check "app/agents/ directory missing"
|
|
156
|
+
fix "rails generate ruby_llm_agents:install"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Helpers
|
|
161
|
+
|
|
162
|
+
def table_exists?(name)
|
|
163
|
+
ActiveRecord::Base.connection.table_exists?(name)
|
|
164
|
+
rescue => e
|
|
165
|
+
say " (Could not check database: #{e.message})", :yellow
|
|
166
|
+
false
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def pass(msg)
|
|
170
|
+
@pass += 1
|
|
171
|
+
say " #{status_icon(:pass)} #{msg}", :green
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def fail_check(msg)
|
|
175
|
+
@fail += 1
|
|
176
|
+
say " #{status_icon(:fail)} #{msg}", :red
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def warn_check(msg)
|
|
180
|
+
@warn += 1
|
|
181
|
+
say " #{status_icon(:warn)} #{msg}", :yellow
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def fix(msg)
|
|
185
|
+
say " Fix: #{msg}"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def status_icon(type)
|
|
189
|
+
case type
|
|
190
|
+
when :pass then "OK"
|
|
191
|
+
when :fail then "FAIL"
|
|
192
|
+
when :warn then "WARN"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -96,28 +96,16 @@ module RubyLlmAgents
|
|
|
96
96
|
say ""
|
|
97
97
|
say "RubyLLM::Agents has been installed!", :green
|
|
98
98
|
say ""
|
|
99
|
-
say "Directory structure created:"
|
|
100
|
-
say " app/"
|
|
101
|
-
say " ├── agents/"
|
|
102
|
-
say " │ ├── application_agent.rb"
|
|
103
|
-
say " │ ├── concerns/"
|
|
104
|
-
say " │ └── AGENTS.md"
|
|
105
|
-
say " └── tools/"
|
|
106
|
-
say " └── TOOLS.md"
|
|
107
|
-
say ""
|
|
108
|
-
say "Skill files (*.md) help AI coding assistants understand how to use this gem."
|
|
109
|
-
say ""
|
|
110
99
|
say "Next steps:"
|
|
111
|
-
say " 1. Set your API
|
|
100
|
+
say " 1. Set your API key in config/initializers/ruby_llm_agents.rb"
|
|
112
101
|
say " 2. Run migrations: rails db:migrate"
|
|
113
|
-
say " 3.
|
|
114
|
-
say " 4.
|
|
102
|
+
say " 3. Verify setup: rails ruby_llm_agents:doctor"
|
|
103
|
+
say " 4. Try it out: rails generate ruby_llm_agents:demo"
|
|
104
|
+
say ""
|
|
105
|
+
say "Or generate a custom agent:"
|
|
106
|
+
say " rails generate ruby_llm_agents:agent MyAgent query:required"
|
|
115
107
|
say ""
|
|
116
|
-
say "
|
|
117
|
-
say " rails generate ruby_llm_agents:agent CustomerSupport query:required"
|
|
118
|
-
say " rails generate ruby_llm_agents:image_generator Product"
|
|
119
|
-
say " rails generate ruby_llm_agents:transcriber Meeting"
|
|
120
|
-
say " rails generate ruby_llm_agents:embedder Semantic"
|
|
108
|
+
say "Dashboard: /agents"
|
|
121
109
|
say ""
|
|
122
110
|
end
|
|
123
111
|
|
data/lib/generators/ruby_llm_agents/templates/add_dashboard_performance_indexes_migration.rb.tt
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Migration to add composite indexes for dashboard query performance
|
|
4
|
+
#
|
|
5
|
+
# These indexes optimize the most frequent dashboard queries:
|
|
6
|
+
# - [status, created_at]: now_strip_data conditional counts, error spike detection, top_errors
|
|
7
|
+
# - [model_id, status]: model_stats GROUP BY with success count
|
|
8
|
+
# - [cache_hit, created_at]: cache_savings queries
|
|
9
|
+
class AddDashboardPerformanceIndexes < ActiveRecord::Migration<%= migration_version %>
|
|
10
|
+
def change
|
|
11
|
+
add_index :ruby_llm_agents_executions, [:status, :created_at],
|
|
12
|
+
name: "idx_executions_status_created_at",
|
|
13
|
+
if_not_exists: true
|
|
14
|
+
|
|
15
|
+
add_index :ruby_llm_agents_executions, [:model_id, :status],
|
|
16
|
+
name: "idx_executions_model_id_status",
|
|
17
|
+
if_not_exists: true
|
|
18
|
+
|
|
19
|
+
add_index :ruby_llm_agents_executions, [:cache_hit, :created_at],
|
|
20
|
+
name: "idx_executions_cache_hit_created_at",
|
|
21
|
+
if_not_exists: true
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -8,108 +8,55 @@
|
|
|
8
8
|
<%- else -%>
|
|
9
9
|
class <%= class_name %>Agent < ApplicationAgent
|
|
10
10
|
<%- end -%>
|
|
11
|
-
|
|
12
|
-
# Model Configuration
|
|
13
|
-
# ============================================
|
|
14
|
-
|
|
11
|
+
<% if options[:model] != "default" -%>
|
|
15
12
|
model "<%= options[:model] %>"
|
|
13
|
+
<% end -%>
|
|
14
|
+
<% if options[:temperature] != 0.0 -%>
|
|
16
15
|
temperature <%= options[:temperature] %>
|
|
17
|
-
# timeout 30 # Per-request timeout in seconds (default: 60)
|
|
18
|
-
|
|
19
|
-
# ============================================
|
|
20
|
-
# Caching
|
|
21
|
-
# ============================================
|
|
22
|
-
|
|
23
|
-
<% if options[:cache] -%>
|
|
24
|
-
cache <%= options[:cache] %>
|
|
25
|
-
<% else -%>
|
|
26
|
-
# cache 1.hour # Enable response caching with TTL
|
|
27
16
|
<% end -%>
|
|
28
17
|
|
|
29
18
|
# ============================================
|
|
30
|
-
#
|
|
19
|
+
# Prompts
|
|
31
20
|
# ============================================
|
|
21
|
+
# Use {placeholder} syntax — placeholders become required params automatically.
|
|
32
22
|
|
|
33
|
-
|
|
34
|
-
# - max: Number of retry attempts
|
|
35
|
-
# - backoff: :constant or :exponential
|
|
36
|
-
# - base: Base delay in seconds
|
|
37
|
-
# - max_delay: Maximum delay between retries
|
|
38
|
-
# - on: Additional error classes to retry on
|
|
39
|
-
# retries max: 2, backoff: :exponential, base: 0.4, max_delay: 3.0
|
|
40
|
-
|
|
41
|
-
# Fallback models (tried in order when primary model fails)
|
|
42
|
-
# fallback_models ["gpt-4o-mini", "claude-3-haiku"]
|
|
43
|
-
|
|
44
|
-
# Total timeout across all retry/fallback attempts
|
|
45
|
-
# total_timeout 30
|
|
46
|
-
|
|
47
|
-
# Circuit breaker (prevents repeated calls to failing models)
|
|
48
|
-
# - errors: Number of errors to trigger open state
|
|
49
|
-
# - within: Rolling window in seconds
|
|
50
|
-
# - cooldown: Time to wait before allowing requests again
|
|
51
|
-
# circuit_breaker errors: 5, within: 60, cooldown: 300
|
|
52
|
-
|
|
53
|
-
# ============================================
|
|
54
|
-
# Parameters
|
|
55
|
-
# ============================================
|
|
23
|
+
system "You are a helpful assistant."
|
|
56
24
|
|
|
57
|
-
<% parsed_params.
|
|
58
|
-
|
|
25
|
+
<% if parsed_params.any? -%>
|
|
26
|
+
prompt "<%= parsed_params.map { |p| "{#{p.name}}" }.join(" ") %>"
|
|
27
|
+
<% else -%>
|
|
28
|
+
prompt "Your prompt here"
|
|
59
29
|
<% end -%>
|
|
60
30
|
|
|
61
|
-
|
|
31
|
+
<% parsed_params.select { |p| !p.required? && p.default }.each do |param| -%>
|
|
32
|
+
param :<%= param.name %>, default: <%= param.default.inspect %>
|
|
33
|
+
<% end -%>
|
|
34
|
+
<% if options[:cache] -%>
|
|
62
35
|
|
|
63
36
|
# ============================================
|
|
64
|
-
#
|
|
37
|
+
# Caching
|
|
65
38
|
# ============================================
|
|
66
39
|
|
|
67
|
-
|
|
68
|
-
<<~PROMPT
|
|
69
|
-
You are a helpful assistant.
|
|
70
|
-
# Define your system instructions here
|
|
71
|
-
PROMPT
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def user_prompt
|
|
75
|
-
# Build the prompt from parameters
|
|
76
|
-
<% if parsed_params.any? -%>
|
|
77
|
-
<%= parsed_params.first.name %>
|
|
78
|
-
<% else -%>
|
|
79
|
-
"Your prompt here"
|
|
40
|
+
cache for: <%= options[:cache] %>
|
|
80
41
|
<% end -%>
|
|
81
|
-
end
|
|
82
42
|
|
|
83
43
|
# ============================================
|
|
84
|
-
#
|
|
44
|
+
# Error Handling (uncomment to enable)
|
|
85
45
|
# ============================================
|
|
86
46
|
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
# integer :confidence, description: "Confidence score 1-100"
|
|
92
|
-
# array :tags, description: "Relevant tags" do
|
|
93
|
-
# string
|
|
94
|
-
# end
|
|
95
|
-
# end
|
|
96
|
-
# end
|
|
97
|
-
|
|
98
|
-
# Custom response processing (default: symbolize hash keys)
|
|
99
|
-
# def process_response(response)
|
|
100
|
-
# content = response.content
|
|
101
|
-
# # Transform or validate the response
|
|
102
|
-
# content
|
|
47
|
+
# on_failure do
|
|
48
|
+
# retries times: 2, backoff: :exponential
|
|
49
|
+
# fallback to: ["gpt-4o-mini"]
|
|
50
|
+
# timeout 30
|
|
103
51
|
# end
|
|
104
52
|
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
# end
|
|
53
|
+
# ============================================
|
|
54
|
+
# Structured Output (uncomment to enable)
|
|
55
|
+
# ============================================
|
|
109
56
|
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
57
|
+
# returns do
|
|
58
|
+
# string :result, description: "The result"
|
|
59
|
+
# integer :confidence, description: "Confidence score 1-100"
|
|
113
60
|
# end
|
|
114
61
|
<%- if class_name.include?("::") -%>
|
|
115
62
|
<%- (class_name.split("::").length - 1).times do |i| -%>
|