ruby_llm-agents 3.11.0 → 3.13.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/app/controllers/ruby_llm/agents/agents_controller.rb +74 -0
- data/app/controllers/ruby_llm/agents/analytics_controller.rb +304 -0
- data/app/controllers/ruby_llm/agents/executions_controller.rb +5 -0
- data/app/controllers/ruby_llm/agents/tenants_controller.rb +74 -2
- data/app/models/ruby_llm/agents/agent_override.rb +47 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +37 -16
- data/app/models/ruby_llm/agents/execution.rb +51 -1
- data/app/services/ruby_llm/agents/agent_registry.rb +8 -1
- data/app/views/layouts/ruby_llm/agents/application.html.erb +4 -2
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +93 -4
- data/app/views/ruby_llm/agents/agents/show.html.erb +17 -2
- data/app/views/ruby_llm/agents/analytics/index.html.erb +398 -0
- data/app/views/ruby_llm/agents/executions/_audio_player.html.erb +1 -1
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +12 -8
- data/app/views/ruby_llm/agents/executions/show.html.erb +26 -12
- data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +46 -7
- data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +2 -2
- data/app/views/ruby_llm/agents/system_config/show.html.erb +6 -2
- data/app/views/ruby_llm/agents/tenants/index.html.erb +3 -2
- data/app/views/ruby_llm/agents/tenants/show.html.erb +225 -0
- data/config/routes.rb +12 -4
- data/lib/generators/ruby_llm_agents/templates/create_overrides_migration.rb.tt +28 -0
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +27 -1
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +1 -1
- data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +1 -1
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +14 -0
- data/lib/ruby_llm/agents/base_agent.rb +90 -133
- data/lib/ruby_llm/agents/core/base.rb +9 -0
- data/lib/ruby_llm/agents/core/configuration.rb +93 -7
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +131 -4
- data/lib/ruby_llm/agents/dsl/knowledge.rb +157 -0
- data/lib/ruby_llm/agents/dsl.rb +1 -1
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +9 -5
- data/lib/ruby_llm/agents/infrastructure/retention_job.rb +118 -0
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +32 -20
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +22 -1
- data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +1 -1
- data/lib/ruby_llm/agents/rails/engine.rb +20 -4
- data/lib/ruby_llm/agents/routing.rb +28 -5
- data/lib/ruby_llm/agents/stream_event.rb +2 -10
- data/lib/ruby_llm/agents/tool.rb +1 -1
- data/lib/ruby_llm/agents.rb +1 -3
- data/lib/tasks/ruby_llm_agents.rake +7 -0
- metadata +9 -5
- data/lib/ruby_llm/agents/agent_tool.rb +0 -143
- data/lib/ruby_llm/agents/dsl/agents.rb +0 -141
|
@@ -79,12 +79,24 @@ module RubyLLM
|
|
|
79
79
|
foreign_key: :execution_id, dependent: :destroy
|
|
80
80
|
|
|
81
81
|
# Delegations so existing code keeps working transparently
|
|
82
|
-
delegate :system_prompt, :user_prompt, :assistant_prompt, :response,
|
|
82
|
+
delegate :system_prompt, :user_prompt, :assistant_prompt, :response,
|
|
83
83
|
:messages_summary, :tool_calls, :attempts, :fallback_chain,
|
|
84
84
|
:parameters, :routed_to, :classification_result,
|
|
85
85
|
:cached_at, :cache_creation_tokens,
|
|
86
86
|
to: :detail, prefix: false, allow_nil: true
|
|
87
87
|
|
|
88
|
+
# Error message reader that survives soft purge.
|
|
89
|
+
#
|
|
90
|
+
# Prefers detail.error_message when the detail row still exists, otherwise
|
|
91
|
+
# falls back to the truncated copy stored in metadata by the retention
|
|
92
|
+
# job. This lets error-rate trend analysis continue working past the
|
|
93
|
+
# soft-purge window.
|
|
94
|
+
#
|
|
95
|
+
# @return [String, nil]
|
|
96
|
+
def error_message
|
|
97
|
+
detail&.error_message || metadata&.dig("error_message")
|
|
98
|
+
end
|
|
99
|
+
|
|
88
100
|
# Validations
|
|
89
101
|
validates :agent_type, :model_id, :started_at, presence: true
|
|
90
102
|
validates :status, inclusion: {in: statuses.keys}
|
|
@@ -215,6 +227,44 @@ module RubyLLM
|
|
|
215
227
|
metadata&.dig("rate_limited") == true
|
|
216
228
|
end
|
|
217
229
|
|
|
230
|
+
# Returns the response payload as a Hash, regardless of how agents wrote it.
|
|
231
|
+
#
|
|
232
|
+
# The `execution_details.response` column is declared as JSON, but agents
|
|
233
|
+
# may write plain strings (chat text), arrays (embeddings), or nil. Views
|
|
234
|
+
# that want to look up specific keys (audio_url, image_url, etc.) need a
|
|
235
|
+
# Hash they can safely `.dig` into. This reader returns an empty hash when
|
|
236
|
+
# the stored response isn't a Hash, so callers don't need type guards.
|
|
237
|
+
#
|
|
238
|
+
# @return [Hash] Parsed response hash, or empty hash if not hash-shaped
|
|
239
|
+
def response_hash
|
|
240
|
+
raw = response
|
|
241
|
+
raw.is_a?(Hash) ? raw : {}
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Returns whether this execution has had its detail payload soft-purged.
|
|
245
|
+
#
|
|
246
|
+
# Soft-purged executions retain all analytics columns (cost, tokens,
|
|
247
|
+
# timing, status) but the large payloads (prompts, responses, tool
|
|
248
|
+
# calls, attempts) stored in execution_details and tool_executions
|
|
249
|
+
# have been destroyed by the retention job.
|
|
250
|
+
#
|
|
251
|
+
# @return [Boolean] true if the retention job has soft-purged this execution
|
|
252
|
+
def soft_purged?
|
|
253
|
+
metadata&.key?("soft_purged_at") == true
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Returns when this execution was soft-purged, if ever.
|
|
257
|
+
#
|
|
258
|
+
# @return [Time, nil] Parsed timestamp or nil if not soft-purged
|
|
259
|
+
def soft_purged_at
|
|
260
|
+
raw = metadata&.dig("soft_purged_at")
|
|
261
|
+
return nil if raw.blank?
|
|
262
|
+
|
|
263
|
+
Time.iso8601(raw)
|
|
264
|
+
rescue ArgumentError
|
|
265
|
+
nil
|
|
266
|
+
end
|
|
267
|
+
|
|
218
268
|
# Convenience accessors for niche fields stored in metadata JSON
|
|
219
269
|
%w[span_id response_cache_key fallback_reason].each do |field|
|
|
220
270
|
define_method(field) { metadata&.dig(field) }
|
|
@@ -70,7 +70,14 @@ module RubyLLM
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
type = detect_agent_type(agent_class)
|
|
73
|
-
base.merge(type_config_for(agent_class, type))
|
|
73
|
+
config = base.merge(type_config_for(agent_class, type))
|
|
74
|
+
|
|
75
|
+
# Include dashboard override metadata
|
|
76
|
+
config[:overridable_fields] = safe_call(agent_class, :overridable_fields) || []
|
|
77
|
+
config[:active_overrides] = safe_call(agent_class, :active_overrides) || {}
|
|
78
|
+
config[:has_overrides] = config[:active_overrides].any?
|
|
79
|
+
|
|
80
|
+
config
|
|
74
81
|
end
|
|
75
82
|
|
|
76
83
|
private
|
|
@@ -296,7 +296,8 @@
|
|
|
296
296
|
[ruby_llm_agents.root_path, "dashboard"],
|
|
297
297
|
[ruby_llm_agents.agents_path, "agents"],
|
|
298
298
|
[ruby_llm_agents.executions_path, "executions"],
|
|
299
|
-
[ruby_llm_agents.requests_path, "requests"]
|
|
299
|
+
[ruby_llm_agents.requests_path, "requests"],
|
|
300
|
+
[ruby_llm_agents.analytics_path, "analytics"]
|
|
300
301
|
]
|
|
301
302
|
nav_items << [ruby_llm_agents.tenants_path, "tenants"] if tenant_filter_enabled?
|
|
302
303
|
nav_items.each do |path, label| %>
|
|
@@ -347,7 +348,8 @@
|
|
|
347
348
|
[ruby_llm_agents.root_path, "dashboard"],
|
|
348
349
|
[ruby_llm_agents.agents_path, "agents"],
|
|
349
350
|
[ruby_llm_agents.executions_path, "executions"],
|
|
350
|
-
[ruby_llm_agents.requests_path, "requests"]
|
|
351
|
+
[ruby_llm_agents.requests_path, "requests"],
|
|
352
|
+
[ruby_llm_agents.analytics_path, "analytics"]
|
|
351
353
|
]
|
|
352
354
|
mobile_nav_items << [ruby_llm_agents.tenants_path, "tenants"] if tenant_filter_enabled?
|
|
353
355
|
mobile_nav_items.each do |path, label| %>
|
|
@@ -7,24 +7,44 @@
|
|
|
7
7
|
has_total_timeout = config[:total_timeout].present?
|
|
8
8
|
has_circuit_breaker = config[:circuit_breaker].present?
|
|
9
9
|
has_any_reliability = has_retries || has_fallbacks || has_total_timeout || has_circuit_breaker
|
|
10
|
+
|
|
11
|
+
overridable = config[:overridable_fields] || []
|
|
12
|
+
overrides = config[:active_overrides] || {}
|
|
13
|
+
has_overrides = overrides.any?
|
|
10
14
|
%>
|
|
11
15
|
|
|
12
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-4 font-mono text-xs">
|
|
16
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-4 font-mono text-xs" x-data="{ editing: false }">
|
|
13
17
|
<!-- Basic -->
|
|
14
18
|
<div>
|
|
15
|
-
<
|
|
16
|
-
|
|
19
|
+
<div class="flex items-center gap-2">
|
|
20
|
+
<span class="text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider">basic</span>
|
|
21
|
+
<% if overridable.any? %>
|
|
22
|
+
<button @click="editing = !editing" class="text-[10px] text-gray-400 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-300 transition-colors" x-text="editing ? 'cancel' : 'edit'"></button>
|
|
23
|
+
<% end %>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<%# Read-only view %>
|
|
27
|
+
<div class="mt-1.5 space-y-0.5" x-show="!editing">
|
|
17
28
|
<div class="flex items-center gap-3 py-1">
|
|
18
29
|
<span class="w-20 text-gray-400 dark:text-gray-600">model</span>
|
|
19
30
|
<span class="text-gray-900 dark:text-gray-200"><%= config[:model] %></span>
|
|
31
|
+
<% if overrides["model"] %>
|
|
32
|
+
<span class="text-[10px] text-yellow-600 dark:text-yellow-500">overridden</span>
|
|
33
|
+
<% end %>
|
|
20
34
|
</div>
|
|
21
35
|
<div class="flex items-center gap-3 py-1">
|
|
22
36
|
<span class="w-20 text-gray-400 dark:text-gray-600">temperature</span>
|
|
23
37
|
<span class="text-gray-900 dark:text-gray-200"><%= config[:temperature] %></span>
|
|
38
|
+
<% if overrides["temperature"] %>
|
|
39
|
+
<span class="text-[10px] text-yellow-600 dark:text-yellow-500">overridden</span>
|
|
40
|
+
<% end %>
|
|
24
41
|
</div>
|
|
25
42
|
<div class="flex items-center gap-3 py-1">
|
|
26
43
|
<span class="w-20 text-gray-400 dark:text-gray-600">timeout</span>
|
|
27
44
|
<span class="text-gray-900 dark:text-gray-200"><%= config[:timeout] %>s</span>
|
|
45
|
+
<% if overrides["timeout"] %>
|
|
46
|
+
<span class="text-[10px] text-yellow-600 dark:text-yellow-500">overridden</span>
|
|
47
|
+
<% end %>
|
|
28
48
|
</div>
|
|
29
49
|
<div class="flex items-center gap-3 py-1">
|
|
30
50
|
<span class="w-20 text-gray-400 dark:text-gray-600">cache</span>
|
|
@@ -36,6 +56,75 @@
|
|
|
36
56
|
<% end %>
|
|
37
57
|
</div>
|
|
38
58
|
</div>
|
|
59
|
+
|
|
60
|
+
<%# Edit form (only shown when editing) %>
|
|
61
|
+
<% if overridable.any? %>
|
|
62
|
+
<div x-show="editing" x-cloak>
|
|
63
|
+
<%= form_tag ruby_llm_agents.agent_path(@agent_type), method: :patch, class: "mt-1.5 space-y-1.5" do %>
|
|
64
|
+
<% if overridable.include?(:model) %>
|
|
65
|
+
<div class="flex items-center gap-3 py-1">
|
|
66
|
+
<label class="w-20 text-gray-400 dark:text-gray-600" for="override_model">model</label>
|
|
67
|
+
<input type="text" name="override[model]" id="override_model"
|
|
68
|
+
value="<%= overrides["model"] || config[:model] %>"
|
|
69
|
+
class="bg-transparent border border-gray-300 dark:border-gray-700 rounded px-2 py-0.5 text-gray-900 dark:text-gray-200 text-xs font-mono w-48 focus:outline-none focus:border-gray-500 dark:focus:border-gray-400">
|
|
70
|
+
</div>
|
|
71
|
+
<% else %>
|
|
72
|
+
<div class="flex items-center gap-3 py-1">
|
|
73
|
+
<span class="w-20 text-gray-400 dark:text-gray-600">model</span>
|
|
74
|
+
<span class="text-gray-900 dark:text-gray-200"><%= config[:model] %></span>
|
|
75
|
+
<span class="text-[10px] text-gray-400 dark:text-gray-600">locked</span>
|
|
76
|
+
</div>
|
|
77
|
+
<% end %>
|
|
78
|
+
|
|
79
|
+
<% if overridable.include?(:temperature) %>
|
|
80
|
+
<div class="flex items-center gap-3 py-1">
|
|
81
|
+
<label class="w-20 text-gray-400 dark:text-gray-600" for="override_temperature">temperature</label>
|
|
82
|
+
<input type="number" name="override[temperature]" id="override_temperature"
|
|
83
|
+
value="<%= overrides["temperature"] || config[:temperature] %>"
|
|
84
|
+
step="0.1" min="0" max="2"
|
|
85
|
+
class="bg-transparent border border-gray-300 dark:border-gray-700 rounded px-2 py-0.5 text-gray-900 dark:text-gray-200 text-xs font-mono w-24 focus:outline-none focus:border-gray-500 dark:focus:border-gray-400">
|
|
86
|
+
</div>
|
|
87
|
+
<% else %>
|
|
88
|
+
<div class="flex items-center gap-3 py-1">
|
|
89
|
+
<span class="w-20 text-gray-400 dark:text-gray-600">temperature</span>
|
|
90
|
+
<span class="text-gray-900 dark:text-gray-200"><%= config[:temperature] %></span>
|
|
91
|
+
<span class="text-[10px] text-gray-400 dark:text-gray-600">locked</span>
|
|
92
|
+
</div>
|
|
93
|
+
<% end %>
|
|
94
|
+
|
|
95
|
+
<% if overridable.include?(:timeout) %>
|
|
96
|
+
<div class="flex items-center gap-3 py-1">
|
|
97
|
+
<label class="w-20 text-gray-400 dark:text-gray-600" for="override_timeout">timeout</label>
|
|
98
|
+
<input type="number" name="override[timeout]" id="override_timeout"
|
|
99
|
+
value="<%= overrides["timeout"] || config[:timeout] %>"
|
|
100
|
+
min="1" max="600"
|
|
101
|
+
class="bg-transparent border border-gray-300 dark:border-gray-700 rounded px-2 py-0.5 text-gray-900 dark:text-gray-200 text-xs font-mono w-24 focus:outline-none focus:border-gray-500 dark:focus:border-gray-400">
|
|
102
|
+
<span class="text-gray-400 dark:text-gray-600">s</span>
|
|
103
|
+
</div>
|
|
104
|
+
<% else %>
|
|
105
|
+
<div class="flex items-center gap-3 py-1">
|
|
106
|
+
<span class="w-20 text-gray-400 dark:text-gray-600">timeout</span>
|
|
107
|
+
<span class="text-gray-900 dark:text-gray-200"><%= config[:timeout] %>s</span>
|
|
108
|
+
<span class="text-[10px] text-gray-400 dark:text-gray-600">locked</span>
|
|
109
|
+
</div>
|
|
110
|
+
<% end %>
|
|
111
|
+
|
|
112
|
+
<div class="flex items-center gap-2 pt-2">
|
|
113
|
+
<button type="submit" class="text-[10px] px-2 py-0.5 rounded bg-gray-800 dark:bg-gray-200 text-white dark:text-gray-900 hover:bg-gray-700 dark:hover:bg-gray-300 transition-colors">save overrides</button>
|
|
114
|
+
</div>
|
|
115
|
+
<% end %>
|
|
116
|
+
|
|
117
|
+
<% if has_overrides %>
|
|
118
|
+
<div class="mt-1.5">
|
|
119
|
+
<%= button_to "reset all", ruby_llm_agents.reset_overrides_agent_path(@agent_type),
|
|
120
|
+
method: :delete,
|
|
121
|
+
class: "text-[10px] text-red-500 hover:text-red-400 transition-colors",
|
|
122
|
+
form: { style: "display:inline" },
|
|
123
|
+
data: { turbo_confirm: "Remove all dashboard overrides for this agent?" } %>
|
|
124
|
+
</div>
|
|
125
|
+
<% end %>
|
|
126
|
+
</div>
|
|
127
|
+
<% end %>
|
|
39
128
|
</div>
|
|
40
129
|
|
|
41
130
|
<!-- Reliability -->
|
|
@@ -56,7 +145,7 @@
|
|
|
56
145
|
<span class="w-1.5 h-1.5 rounded-full flex-shrink-0 <%= has_fallbacks ? 'bg-green-500' : 'bg-gray-400' %>"></span>
|
|
57
146
|
<span class="w-16 text-gray-400 dark:text-gray-600">fallbacks</span>
|
|
58
147
|
<% if has_fallbacks %>
|
|
59
|
-
<span class="text-gray-900 dark:text-gray-200 truncate"><%= fallback_models.join("
|
|
148
|
+
<span class="text-gray-900 dark:text-gray-200 truncate"><%= fallback_models.join(" → ") %></span>
|
|
60
149
|
<% else %>
|
|
61
150
|
<span class="text-gray-400 dark:text-gray-600">—</span>
|
|
62
151
|
<% end %>
|
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
<%= link_to "← agents", ruby_llm_agents.agents_path(subtab: params[:subtab]), class: "text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" %>
|
|
4
4
|
</nav>
|
|
5
5
|
|
|
6
|
+
<%# Override banner %>
|
|
7
|
+
<% if @config && @config[:has_overrides] %>
|
|
8
|
+
<div class="flex items-center gap-2 px-3 py-1.5 mb-4 rounded border border-yellow-300 dark:border-yellow-700 bg-yellow-50 dark:bg-yellow-900/20 font-mono text-xs text-yellow-700 dark:text-yellow-400">
|
|
9
|
+
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"></path></svg>
|
|
10
|
+
<span>dashboard overrides active: <strong><%= @config[:active_overrides].keys.join(", ") %></strong></span>
|
|
11
|
+
<%= button_to "reset", ruby_llm_agents.reset_overrides_agent_path(@agent_type),
|
|
12
|
+
method: :delete,
|
|
13
|
+
class: "ml-auto text-yellow-600 dark:text-yellow-500 hover:text-yellow-800 dark:hover:text-yellow-300",
|
|
14
|
+
form: { style: "display:inline" },
|
|
15
|
+
data: { turbo_confirm: "Remove all dashboard overrides?" } %>
|
|
16
|
+
</div>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
6
19
|
<!-- ── agent header ──────────────── -->
|
|
7
20
|
<div class="flex items-start justify-between gap-8 mb-2">
|
|
8
21
|
<!-- Left: identity -->
|
|
@@ -67,6 +80,8 @@
|
|
|
67
80
|
<span><span class="text-gray-800 dark:text-gray-200">$<%= number_with_precision(@stats[:total_cost] || 0, precision: 4) %></span> cost</span>
|
|
68
81
|
</div>
|
|
69
82
|
<div class="flex items-center justify-end gap-x-4 text-gray-500 dark:text-gray-600">
|
|
83
|
+
<span><span class="text-gray-600 dark:text-gray-400">$<%= number_with_precision(@stats[:avg_cost] || 0, precision: 4) %></span> avg cost</span>
|
|
84
|
+
<span><span class="text-gray-600 dark:text-gray-400"><%= number_with_delimiter(@stats[:avg_tokens] || 0) %></span> avg tokens</span>
|
|
70
85
|
<span><span class="text-gray-600 dark:text-gray-400"><%= number_with_delimiter(@stats[:total_tokens] || 0) %></span> tokens</span>
|
|
71
86
|
<span><span class="text-gray-600 dark:text-gray-400"><%= number_with_delimiter(@stats[:avg_duration_ms] || 0) %></span>ms avg</span>
|
|
72
87
|
<% if (@config && @config[:cache_enabled]) || @cache_hit_rate > 0 %>
|
|
@@ -279,8 +294,8 @@
|
|
|
279
294
|
<% end %>
|
|
280
295
|
<% end %>
|
|
281
296
|
|
|
282
|
-
<!-- ── config
|
|
283
|
-
<% if @config && @agent_type_kind
|
|
297
|
+
<!-- ── config ── -->
|
|
298
|
+
<% if @config && @agent_type_kind %>
|
|
284
299
|
<div class="flex items-center gap-3 mt-6 mb-3">
|
|
285
300
|
<span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">config</span>
|
|
286
301
|
<div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
|