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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -10
  3. data/app/controllers/ruby_llm/agents/agents_controller.rb +14 -141
  4. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +12 -166
  5. data/app/controllers/ruby_llm/agents/executions_controller.rb +1 -1
  6. data/app/controllers/ruby_llm/agents/requests_controller.rb +117 -0
  7. data/app/helpers/ruby_llm/agents/application_helper.rb +38 -0
  8. data/app/models/ruby_llm/agents/execution/analytics.rb +302 -103
  9. data/app/models/ruby_llm/agents/execution.rb +76 -54
  10. data/app/models/ruby_llm/agents/execution_detail.rb +2 -0
  11. data/app/models/ruby_llm/agents/tenant.rb +39 -0
  12. data/app/services/ruby_llm/agents/agent_registry.rb +98 -0
  13. data/app/views/layouts/ruby_llm/agents/application.html.erb +4 -2
  14. data/app/views/ruby_llm/agents/executions/_list.html.erb +3 -17
  15. data/app/views/ruby_llm/agents/requests/index.html.erb +153 -0
  16. data/app/views/ruby_llm/agents/requests/show.html.erb +136 -0
  17. data/config/routes.rb +2 -0
  18. data/lib/generators/ruby_llm_agents/agent_generator.rb +2 -2
  19. data/lib/generators/ruby_llm_agents/demo_generator.rb +102 -0
  20. data/lib/generators/ruby_llm_agents/doctor_generator.rb +196 -0
  21. data/lib/generators/ruby_llm_agents/install_generator.rb +7 -19
  22. data/lib/generators/ruby_llm_agents/templates/add_dashboard_performance_indexes_migration.rb.tt +23 -0
  23. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +27 -80
  24. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +18 -51
  25. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +19 -17
  26. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +3 -0
  27. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +25 -0
  28. data/lib/ruby_llm/agents/base_agent.rb +71 -4
  29. data/lib/ruby_llm/agents/core/base.rb +4 -0
  30. data/lib/ruby_llm/agents/core/configuration.rb +11 -0
  31. data/lib/ruby_llm/agents/core/instrumentation.rb +15 -19
  32. data/lib/ruby_llm/agents/core/version.rb +1 -1
  33. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +4 -4
  34. data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +19 -11
  35. data/lib/ruby_llm/agents/pipeline/builder.rb +8 -4
  36. data/lib/ruby_llm/agents/pipeline/context.rb +69 -1
  37. data/lib/ruby_llm/agents/pipeline/middleware/base.rb +58 -4
  38. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +21 -17
  39. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +40 -26
  40. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +126 -120
  41. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +13 -11
  42. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +29 -31
  43. data/lib/ruby_llm/agents/providers/inception/capabilities.rb +107 -0
  44. data/lib/ruby_llm/agents/providers/inception/chat.rb +17 -0
  45. data/lib/ruby_llm/agents/providers/inception/configuration.rb +9 -0
  46. data/lib/ruby_llm/agents/providers/inception/models.rb +38 -0
  47. data/lib/ruby_llm/agents/providers/inception/registry.rb +45 -0
  48. data/lib/ruby_llm/agents/providers/inception.rb +50 -0
  49. data/lib/ruby_llm/agents/rails/engine.rb +11 -0
  50. data/lib/ruby_llm/agents/results/background_removal_result.rb +7 -1
  51. data/lib/ruby_llm/agents/results/base.rb +28 -4
  52. data/lib/ruby_llm/agents/results/embedding_result.rb +4 -0
  53. data/lib/ruby_llm/agents/results/image_analysis_result.rb +11 -3
  54. data/lib/ruby_llm/agents/results/image_edit_result.rb +7 -1
  55. data/lib/ruby_llm/agents/results/image_generation_result.rb +7 -1
  56. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +7 -1
  57. data/lib/ruby_llm/agents/results/image_transform_result.rb +7 -1
  58. data/lib/ruby_llm/agents/results/image_upscale_result.rb +7 -1
  59. data/lib/ruby_llm/agents/results/image_variation_result.rb +7 -1
  60. data/lib/ruby_llm/agents/results/speech_result.rb +6 -0
  61. data/lib/ruby_llm/agents/results/trackable.rb +25 -0
  62. data/lib/ruby_llm/agents/results/transcription_result.rb +6 -0
  63. data/lib/ruby_llm/agents/text/embedder.rb +8 -1
  64. data/lib/ruby_llm/agents/track_report.rb +127 -0
  65. data/lib/ruby_llm/agents/tracker.rb +32 -0
  66. data/lib/ruby_llm/agents.rb +212 -0
  67. data/lib/tasks/ruby_llm_agents.rake +6 -0
  68. 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">&#x2514;</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
@@ -13,6 +13,8 @@ RubyLLM::Agents::Engine.routes.draw do
13
13
  end
14
14
  end
15
15
 
16
+ resources :requests, only: [:index, :show]
17
+
16
18
  resources :tenants, only: [:index, :show, :edit, :update]
17
19
 
18
20
  # Redirect old analytics route to dashboard
@@ -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: "gemini-2.0-flash",
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 keys in config/initializers/ruby_llm_agents.rb"
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. Generate an agent: rails generate ruby_llm_agents:agent MyAgent query:required"
114
- say " 4. Access the dashboard at: /agents"
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 "Generator commands:"
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
 
@@ -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
- # Reliability (Retries & Fallbacks)
19
+ # Prompts
31
20
  # ============================================
21
+ # Use {placeholder} syntax — placeholders become required params automatically.
32
22
 
33
- # Automatic retries with exponential backoff
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.each do |param| -%>
58
- param :<%= param.name %><%= ", required: true" if param.required? %><%= ", default: #{param.default.inspect}" if param.default && !param.required? %>
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
- private
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
- # Prompts (required)
37
+ # Caching
65
38
  # ============================================
66
39
 
67
- def system_prompt
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
- # Optional Overrides
44
+ # Error Handling (uncomment to enable)
85
45
  # ============================================
86
46
 
87
- # Structured output schema (returns parsed hash instead of raw text)
88
- # def schema
89
- # @schema ||= RubyLLM::Schema.create do
90
- # string :result, description: "The result"
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
- # Custom metadata to include in execution logs
106
- # def metadata
107
- # { custom_field: "value", request_id: params[:request_id] }
108
- # end
53
+ # ============================================
54
+ # Structured Output (uncomment to enable)
55
+ # ============================================
109
56
 
110
- # Custom cache key data (default: all params except skip_cache, dry_run)
111
- # def cache_key_data
112
- # { query: params[:query], locale: I18n.locale }
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| -%>