ruby_llm-agents 0.3.0 → 0.3.3
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 +228 -0
- data/app/models/ruby_llm/agents/execution/scopes.rb +10 -0
- data/app/models/ruby_llm/agents/execution.rb +7 -0
- data/app/views/layouts/rubyllm/agents/application.html.erb +11 -2
- data/app/views/rubyllm/agents/agents/_agent.html.erb +87 -0
- data/app/views/rubyllm/agents/agents/index.html.erb +2 -71
- data/app/views/rubyllm/agents/agents/show.html.erb +20 -33
- data/app/views/rubyllm/agents/dashboard/_action_center.html.erb +7 -7
- data/app/views/rubyllm/agents/dashboard/_execution_item.html.erb +54 -39
- data/app/views/rubyllm/agents/dashboard/index.html.erb +4 -34
- data/app/views/rubyllm/agents/executions/show.html.erb +166 -52
- data/lib/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt +28 -0
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +4 -0
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +7 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +13 -0
- data/lib/ruby_llm/agents/base.rb +227 -20
- data/lib/ruby_llm/agents/instrumentation.rb +36 -3
- data/lib/ruby_llm/agents/redactor.rb +1 -1
- data/lib/ruby_llm/agents/result.rb +235 -0
- data/lib/ruby_llm/agents/version.rb +1 -1
- data/lib/ruby_llm/agents.rb +1 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 26b04f0b8d00a9c87b5555beccacbfc83208be0a370b0fcc6e7ee518c2543282
|
|
4
|
+
data.tar.gz: 51a1d674af7e489ad6020eb324b13b6509137e035c45bb53d140ca22574fc187
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 69635d4258f0082742d1b899e26e21019020e58f904df3a20833c7d7035dc39f139a4d07568a598735b4071e694dff48cf7ca67b37f04ae3f6341d9c32e30f8c
|
|
7
|
+
data.tar.gz: f655a50e12ce7117f34bf7a1d0cbc3c8b7168233c6fd4ebcd63e5678de92b0d5fad61152f4001a173b201673dcd87380f4a22faa9b6d542210428e3896b7482d
|
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# RubyLLM::Agents
|
|
2
2
|
|
|
3
|
+
[](https://badge.fury.io/rb/ruby_llm-agents)
|
|
4
|
+
|
|
3
5
|
A powerful Rails engine for building, managing, and monitoring LLM-powered agents using [RubyLLM](https://github.com/crmne/ruby_llm).
|
|
4
6
|
|
|
5
7
|
## Features
|
|
@@ -12,6 +14,9 @@ A powerful Rails engine for building, managing, and monitoring LLM-powered agent
|
|
|
12
14
|
- **🛠️ Generators** - Quickly scaffold new agents with customizable templates
|
|
13
15
|
- **🔍 Anomaly Detection** - Automatic warnings for unusual cost or duration patterns
|
|
14
16
|
- **🎯 Type Safety** - Structured output with RubyLLM::Schema integration
|
|
17
|
+
- **⚡ Real-time Streaming** - Stream LLM responses with time-to-first-token tracking
|
|
18
|
+
- **📎 Attachments** - Send images, PDFs, and files to vision-capable models
|
|
19
|
+
- **📋 Rich Results** - Access token counts, costs, timing, and model info from every execution
|
|
15
20
|
- **🔄 Reliability** - Automatic retries, model fallbacks, and circuit breakers for resilient agents
|
|
16
21
|
- **💵 Budget Controls** - Daily/monthly spending limits with hard and soft enforcement
|
|
17
22
|
- **🔔 Alerts** - Slack, webhook, and custom notifications for budget and circuit breaker events
|
|
@@ -150,6 +155,210 @@ SearchIntentAgent.call(query: "test", dry_run: true)
|
|
|
150
155
|
SearchIntentAgent.call(query: "test", skip_cache: true)
|
|
151
156
|
```
|
|
152
157
|
|
|
158
|
+
### Streaming Responses
|
|
159
|
+
|
|
160
|
+
Enable real-time streaming to receive LLM responses as they're generated:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
class StreamingAgent < ApplicationAgent
|
|
164
|
+
model "gpt-4o"
|
|
165
|
+
streaming true # Enable streaming for this agent
|
|
166
|
+
|
|
167
|
+
param :prompt, required: true
|
|
168
|
+
|
|
169
|
+
def user_prompt
|
|
170
|
+
prompt
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### Using Streaming with a Block
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
# Stream responses in real-time
|
|
179
|
+
StreamingAgent.call(prompt: "Write a story") do |chunk|
|
|
180
|
+
print chunk # Process each chunk as it arrives
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### HTTP Streaming with ActionController::Live
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
class StreamingController < ApplicationController
|
|
188
|
+
include ActionController::Live
|
|
189
|
+
|
|
190
|
+
def stream_response
|
|
191
|
+
response.headers['Content-Type'] = 'text/event-stream'
|
|
192
|
+
response.headers['Cache-Control'] = 'no-cache'
|
|
193
|
+
|
|
194
|
+
StreamingAgent.call(prompt: params[:prompt]) do |chunk|
|
|
195
|
+
response.stream.write "data: #{chunk}\n\n"
|
|
196
|
+
end
|
|
197
|
+
ensure
|
|
198
|
+
response.stream.close
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### Time-to-First-Token Tracking
|
|
204
|
+
|
|
205
|
+
Streaming executions automatically track latency metrics:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
execution = RubyLLM::Agents::Execution.last
|
|
209
|
+
execution.streaming? # => true
|
|
210
|
+
execution.time_to_first_token_ms # => 245 (milliseconds to first chunk)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
#### Global Streaming Configuration
|
|
214
|
+
|
|
215
|
+
Enable streaming by default for all agents:
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
# config/initializers/ruby_llm_agents.rb
|
|
219
|
+
RubyLLM::Agents.configure do |config|
|
|
220
|
+
config.default_streaming = true
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Attachments (Vision & Multimodal)
|
|
225
|
+
|
|
226
|
+
Send images, PDFs, and other files to vision-capable models using the `with:` option:
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
class VisionAgent < ApplicationAgent
|
|
230
|
+
model "gpt-4o" # Use a vision-capable model
|
|
231
|
+
param :question, required: true
|
|
232
|
+
|
|
233
|
+
def user_prompt
|
|
234
|
+
question
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### Single Attachment
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
# Local file
|
|
243
|
+
VisionAgent.call(question: "Describe this image", with: "photo.jpg")
|
|
244
|
+
|
|
245
|
+
# URL
|
|
246
|
+
VisionAgent.call(question: "What architecture is shown?", with: "https://example.com/building.jpg")
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### Multiple Attachments
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
VisionAgent.call(
|
|
253
|
+
question: "Compare these two screenshots",
|
|
254
|
+
with: ["screenshot_v1.png", "screenshot_v2.png"]
|
|
255
|
+
)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
#### Supported File Types
|
|
259
|
+
|
|
260
|
+
RubyLLM automatically detects file types:
|
|
261
|
+
|
|
262
|
+
- **Images:** `.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`, `.bmp`
|
|
263
|
+
- **Videos:** `.mp4`, `.mov`, `.avi`, `.webm`
|
|
264
|
+
- **Audio:** `.mp3`, `.wav`, `.m4a`, `.ogg`, `.flac`
|
|
265
|
+
- **Documents:** `.pdf`, `.txt`, `.md`, `.csv`, `.json`, `.xml`
|
|
266
|
+
- **Code:** `.rb`, `.py`, `.js`, `.html`, `.css`, and many others
|
|
267
|
+
|
|
268
|
+
#### Debug Mode with Attachments
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
VisionAgent.call(question: "test", with: "image.png", dry_run: true)
|
|
272
|
+
# => { ..., attachments: "image.png", ... }
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Execution Results
|
|
276
|
+
|
|
277
|
+
Every agent call returns a `Result` object with full execution metadata:
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
result = SearchAgent.call(query: "red dress")
|
|
281
|
+
|
|
282
|
+
# Access the processed response
|
|
283
|
+
result.content # => { refined_query: "red dress", ... }
|
|
284
|
+
|
|
285
|
+
# Token usage
|
|
286
|
+
result.input_tokens # => 150
|
|
287
|
+
result.output_tokens # => 50
|
|
288
|
+
result.total_tokens # => 200
|
|
289
|
+
result.cached_tokens # => 0
|
|
290
|
+
|
|
291
|
+
# Cost calculation
|
|
292
|
+
result.input_cost # => 0.000150
|
|
293
|
+
result.output_cost # => 0.000100
|
|
294
|
+
result.total_cost # => 0.000250
|
|
295
|
+
|
|
296
|
+
# Model info
|
|
297
|
+
result.model_id # => "gpt-4o"
|
|
298
|
+
result.chosen_model_id # => "gpt-4o" (may differ if fallback used)
|
|
299
|
+
result.temperature # => 0.0
|
|
300
|
+
|
|
301
|
+
# Timing
|
|
302
|
+
result.duration_ms # => 1234
|
|
303
|
+
result.started_at # => 2025-11-27 10:30:00 UTC
|
|
304
|
+
result.completed_at # => 2025-11-27 10:30:01 UTC
|
|
305
|
+
result.time_to_first_token_ms # => 245 (streaming only)
|
|
306
|
+
|
|
307
|
+
# Status
|
|
308
|
+
result.finish_reason # => "stop", "length", "tool_calls", etc.
|
|
309
|
+
result.streaming? # => false
|
|
310
|
+
result.success? # => true
|
|
311
|
+
result.truncated? # => false (true if hit max_tokens)
|
|
312
|
+
|
|
313
|
+
# Tool calls (for agents with tools)
|
|
314
|
+
result.tool_calls # => [{ "id" => "call_abc", "name" => "search", "arguments" => {...} }]
|
|
315
|
+
result.tool_calls_count # => 1
|
|
316
|
+
result.has_tool_calls? # => true
|
|
317
|
+
|
|
318
|
+
# Reliability info
|
|
319
|
+
result.attempts_count # => 1
|
|
320
|
+
result.used_fallback? # => false
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
#### Backward Compatibility
|
|
324
|
+
|
|
325
|
+
The Result object delegates hash methods to content, so existing code continues to work:
|
|
326
|
+
|
|
327
|
+
```ruby
|
|
328
|
+
# Old style (still works)
|
|
329
|
+
result[:refined_query]
|
|
330
|
+
result.dig(:nested, :key)
|
|
331
|
+
|
|
332
|
+
# New style (access metadata)
|
|
333
|
+
result.content[:refined_query]
|
|
334
|
+
result.total_cost
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
#### Full Metadata Hash
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
result.to_h
|
|
341
|
+
# => {
|
|
342
|
+
# content: { refined_query: "red dress", ... },
|
|
343
|
+
# input_tokens: 150,
|
|
344
|
+
# output_tokens: 50,
|
|
345
|
+
# total_tokens: 200,
|
|
346
|
+
# cached_tokens: 0,
|
|
347
|
+
# input_cost: 0.000150,
|
|
348
|
+
# output_cost: 0.000100,
|
|
349
|
+
# total_cost: 0.000250,
|
|
350
|
+
# model_id: "gpt-4o",
|
|
351
|
+
# chosen_model_id: "gpt-4o",
|
|
352
|
+
# temperature: 0.0,
|
|
353
|
+
# duration_ms: 1234,
|
|
354
|
+
# finish_reason: "stop",
|
|
355
|
+
# streaming: false,
|
|
356
|
+
# tool_calls: [...],
|
|
357
|
+
# tool_calls_count: 0,
|
|
358
|
+
# ...
|
|
359
|
+
# }
|
|
360
|
+
```
|
|
361
|
+
|
|
153
362
|
## Usage Guide
|
|
154
363
|
|
|
155
364
|
### Agent DSL
|
|
@@ -680,6 +889,9 @@ RubyLLM::Agents.configure do |config|
|
|
|
680
889
|
# Default timeout for LLM requests (in seconds)
|
|
681
890
|
config.default_timeout = 60
|
|
682
891
|
|
|
892
|
+
# Enable streaming by default for all agents
|
|
893
|
+
config.default_streaming = false
|
|
894
|
+
|
|
683
895
|
# ============================================================================
|
|
684
896
|
# Caching Configuration
|
|
685
897
|
# ============================================================================
|
|
@@ -866,6 +1078,18 @@ trend = RubyLLM::Agents::Execution.trend_analysis(
|
|
|
866
1078
|
# ]
|
|
867
1079
|
```
|
|
868
1080
|
|
|
1081
|
+
### Streaming Analytics
|
|
1082
|
+
|
|
1083
|
+
```ruby
|
|
1084
|
+
# Percentage of executions using streaming
|
|
1085
|
+
RubyLLM::Agents::Execution.streaming_rate
|
|
1086
|
+
# => 45.5
|
|
1087
|
+
|
|
1088
|
+
# Average time-to-first-token for streaming executions (milliseconds)
|
|
1089
|
+
RubyLLM::Agents::Execution.avg_time_to_first_token
|
|
1090
|
+
# => 245.3
|
|
1091
|
+
```
|
|
1092
|
+
|
|
869
1093
|
### Scopes
|
|
870
1094
|
|
|
871
1095
|
Chain scopes for complex queries:
|
|
@@ -919,6 +1143,10 @@ expensive_slow_failures = RubyLLM::Agents::Execution
|
|
|
919
1143
|
|
|
920
1144
|
# Token usage
|
|
921
1145
|
.high_token_usage(threshold)
|
|
1146
|
+
|
|
1147
|
+
# Streaming
|
|
1148
|
+
.streaming
|
|
1149
|
+
.non_streaming
|
|
922
1150
|
```
|
|
923
1151
|
|
|
924
1152
|
## Generators
|
|
@@ -256,6 +256,16 @@ module RubyLLM
|
|
|
256
256
|
scope :content_filtered, -> { where(finish_reason: "content_filter") }
|
|
257
257
|
scope :tool_calls, -> { where(finish_reason: "tool_calls") }
|
|
258
258
|
|
|
259
|
+
# @!method with_tool_calls
|
|
260
|
+
# Returns executions that made tool calls
|
|
261
|
+
# @return [ActiveRecord::Relation]
|
|
262
|
+
|
|
263
|
+
# @!method without_tool_calls
|
|
264
|
+
# Returns executions that did not make tool calls
|
|
265
|
+
# @return [ActiveRecord::Relation]
|
|
266
|
+
scope :with_tool_calls, -> { where("tool_calls_count > 0") }
|
|
267
|
+
scope :without_tool_calls, -> { where(tool_calls_count: 0) }
|
|
268
|
+
|
|
259
269
|
# @!endgroup
|
|
260
270
|
end
|
|
261
271
|
|
|
@@ -219,6 +219,13 @@ module RubyLLM
|
|
|
219
219
|
finish_reason == "content_filter"
|
|
220
220
|
end
|
|
221
221
|
|
|
222
|
+
# Returns whether this execution made tool calls
|
|
223
|
+
#
|
|
224
|
+
# @return [Boolean] true if tool calls were made
|
|
225
|
+
def has_tool_calls?
|
|
226
|
+
tool_calls_count.to_i > 0
|
|
227
|
+
end
|
|
228
|
+
|
|
222
229
|
# Returns real-time dashboard data for the Now Strip
|
|
223
230
|
#
|
|
224
231
|
# @return [Hash] Now strip metrics
|
|
@@ -322,6 +322,15 @@
|
|
|
322
322
|
.badge-timeout {
|
|
323
323
|
@apply bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200;
|
|
324
324
|
}
|
|
325
|
+
.badge-cyan {
|
|
326
|
+
@apply bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300;
|
|
327
|
+
}
|
|
328
|
+
.badge-purple {
|
|
329
|
+
@apply bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300;
|
|
330
|
+
}
|
|
331
|
+
.badge-orange {
|
|
332
|
+
@apply bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300;
|
|
333
|
+
}
|
|
325
334
|
</style>
|
|
326
335
|
</head>
|
|
327
336
|
|
|
@@ -581,7 +590,7 @@
|
|
|
581
590
|
data-controller="theme"
|
|
582
591
|
>
|
|
583
592
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
584
|
-
<div class="flex items-center justify-between">
|
|
593
|
+
<div class="flex flex-col sm:flex-row items-center sm:justify-between gap-3 sm:gap-0">
|
|
585
594
|
<div class="flex items-center space-x-2">
|
|
586
595
|
<label
|
|
587
596
|
for="theme-select"
|
|
@@ -606,7 +615,7 @@
|
|
|
606
615
|
</select>
|
|
607
616
|
</div>
|
|
608
617
|
|
|
609
|
-
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
618
|
+
<p class="text-sm text-gray-500 dark:text-gray-400 text-center">
|
|
610
619
|
Powered by
|
|
611
620
|
<a href="https://github.com/adham90/ruby_llm-agents" class="text-blue-600 dark:text-blue-400 hover:underline">ruby_llm-agents</a>
|
|
612
621
|
</p>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<%= link_to ruby_llm_agents.agent_path(agent[:name]), class: "block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow" do %>
|
|
2
|
+
<div class="p-4 sm:p-5">
|
|
3
|
+
<!-- Row 1: Agent name + badge + model/timestamp -->
|
|
4
|
+
<div class="flex items-center justify-between gap-2">
|
|
5
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
6
|
+
<h3 class="font-semibold text-gray-900 dark:text-gray-100 truncate">
|
|
7
|
+
<%= agent[:name].gsub(/Agent$/, '') %>
|
|
8
|
+
</h3>
|
|
9
|
+
<span class="hidden sm:inline text-sm text-gray-500 dark:text-gray-400">v<%= agent[:version] %></span>
|
|
10
|
+
<% if agent[:active] %>
|
|
11
|
+
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300">Active</span>
|
|
12
|
+
<% else %>
|
|
13
|
+
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">Deleted</span>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
16
|
+
<!-- Desktop: model -->
|
|
17
|
+
<span class="hidden sm:block text-sm text-gray-500 dark:text-gray-400"><%= agent[:model] %></span>
|
|
18
|
+
<!-- Mobile: last executed -->
|
|
19
|
+
<span class="sm:hidden text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">
|
|
20
|
+
<% if agent[:last_executed] %>
|
|
21
|
+
<%= time_ago_in_words(agent[:last_executed]) %> ago
|
|
22
|
+
<% else %>
|
|
23
|
+
Never
|
|
24
|
+
<% end %>
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<!-- Row 2: Stats -->
|
|
29
|
+
<div class="mt-2 sm:mt-3 sm:border-t sm:border-gray-100 sm:dark:border-gray-700 sm:pt-3">
|
|
30
|
+
<!-- Mobile: compact inline -->
|
|
31
|
+
<% success_rate = agent[:success_rate] || 0 %>
|
|
32
|
+
<div class="sm:hidden text-xs text-gray-500 dark:text-gray-400">
|
|
33
|
+
<%= number_with_delimiter(agent[:execution_count]) %> runs
|
|
34
|
+
<span class="mx-1 text-gray-300 dark:text-gray-600">·</span>
|
|
35
|
+
$<%= number_with_precision(agent[:total_cost] || 0, precision: 2) %>
|
|
36
|
+
<span class="mx-1 text-gray-300 dark:text-gray-600">·</span>
|
|
37
|
+
<span class="<%= success_rate >= 95 ? 'text-green-600 dark:text-green-400' : success_rate >= 80 ? 'text-yellow-600 dark:text-yellow-400' : 'text-red-600 dark:text-red-400' %>">
|
|
38
|
+
<%= success_rate %>%
|
|
39
|
+
</span>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<!-- Desktop: full stats with icons -->
|
|
43
|
+
<div class="hidden sm:flex items-center justify-between text-sm">
|
|
44
|
+
<div class="flex items-center space-x-6">
|
|
45
|
+
<!-- Executions -->
|
|
46
|
+
<div class="flex items-center text-gray-600 dark:text-gray-300">
|
|
47
|
+
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
48
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
49
|
+
</svg>
|
|
50
|
+
<span><%= number_with_delimiter(agent[:execution_count]) %> executions</span>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Cost -->
|
|
54
|
+
<div class="flex items-center text-gray-600 dark:text-gray-300">
|
|
55
|
+
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
56
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
57
|
+
</svg>
|
|
58
|
+
<span>$<%= number_with_precision(agent[:total_cost] || 0, precision: 4) %></span>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<!-- Success Rate -->
|
|
62
|
+
<div class="flex items-center">
|
|
63
|
+
<% if success_rate >= 95 %>
|
|
64
|
+
<span class="w-2 h-2 rounded-full bg-green-500 mr-1.5"></span>
|
|
65
|
+
<span class="text-green-600 dark:text-green-400"><%= success_rate %>%</span>
|
|
66
|
+
<% elsif success_rate >= 80 %>
|
|
67
|
+
<span class="w-2 h-2 rounded-full bg-yellow-500 mr-1.5"></span>
|
|
68
|
+
<span class="text-yellow-600 dark:text-yellow-400"><%= success_rate %>%</span>
|
|
69
|
+
<% else %>
|
|
70
|
+
<span class="w-2 h-2 rounded-full bg-red-500 mr-1.5"></span>
|
|
71
|
+
<span class="text-red-600 dark:text-red-400"><%= success_rate %>%</span>
|
|
72
|
+
<% end %>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<!-- Last Executed -->
|
|
77
|
+
<div class="text-gray-400 dark:text-gray-500">
|
|
78
|
+
<% if agent[:last_executed] %>
|
|
79
|
+
<%= time_ago_in_words(agent[:last_executed]) %> ago
|
|
80
|
+
<% else %>
|
|
81
|
+
Never executed
|
|
82
|
+
<% end %>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
<% end %>
|
|
@@ -12,78 +12,9 @@
|
|
|
12
12
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Create an agent by running <code class="bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-1 rounded">rails g ruby_llm_agents:agent YourAgentName</code></p>
|
|
13
13
|
</div>
|
|
14
14
|
<% else %>
|
|
15
|
-
<div class="space-y-4">
|
|
15
|
+
<div class="space-y-3 sm:space-y-4">
|
|
16
16
|
<% @agents.each do |agent| %>
|
|
17
|
-
<%=
|
|
18
|
-
<div class="p-5">
|
|
19
|
-
<!-- Header Row -->
|
|
20
|
-
<div class="flex items-start justify-between mb-3">
|
|
21
|
-
<div class="flex items-center space-x-3">
|
|
22
|
-
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
|
23
|
-
<%= agent[:name].gsub(/Agent$/, '') %>
|
|
24
|
-
</h3>
|
|
25
|
-
<span class="text-sm text-gray-500 dark:text-gray-400">v<%= agent[:version] %></span>
|
|
26
|
-
<% if agent[:active] %>
|
|
27
|
-
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300">
|
|
28
|
-
Active
|
|
29
|
-
</span>
|
|
30
|
-
<% else %>
|
|
31
|
-
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
|
|
32
|
-
Deleted
|
|
33
|
-
</span>
|
|
34
|
-
<% end %>
|
|
35
|
-
</div>
|
|
36
|
-
<div class="text-right">
|
|
37
|
-
<p class="text-sm text-gray-500 dark:text-gray-400"><%= agent[:model] %></p>
|
|
38
|
-
</div>
|
|
39
|
-
</div>
|
|
40
|
-
|
|
41
|
-
<!-- Stats Row -->
|
|
42
|
-
<div class="flex items-center justify-between text-sm border-t border-gray-100 dark:border-gray-700 pt-3">
|
|
43
|
-
<div class="flex items-center space-x-6">
|
|
44
|
-
<!-- Executions -->
|
|
45
|
-
<div class="flex items-center text-gray-600 dark:text-gray-300">
|
|
46
|
-
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
47
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
48
|
-
</svg>
|
|
49
|
-
<span><%= number_with_delimiter(agent[:execution_count]) %> executions</span>
|
|
50
|
-
</div>
|
|
51
|
-
|
|
52
|
-
<!-- Cost -->
|
|
53
|
-
<div class="flex items-center text-gray-600 dark:text-gray-300">
|
|
54
|
-
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
55
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
56
|
-
</svg>
|
|
57
|
-
<span>$<%= number_with_precision(agent[:total_cost] || 0, precision: 4) %></span>
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
<!-- Success Rate -->
|
|
61
|
-
<div class="flex items-center">
|
|
62
|
-
<% success_rate = agent[:success_rate] || 0 %>
|
|
63
|
-
<% if success_rate >= 95 %>
|
|
64
|
-
<span class="w-2 h-2 rounded-full bg-green-500 mr-1.5"></span>
|
|
65
|
-
<span class="text-green-600 dark:text-green-400"><%= success_rate %>%</span>
|
|
66
|
-
<% elsif success_rate >= 80 %>
|
|
67
|
-
<span class="w-2 h-2 rounded-full bg-yellow-500 mr-1.5"></span>
|
|
68
|
-
<span class="text-yellow-600 dark:text-yellow-400"><%= success_rate %>%</span>
|
|
69
|
-
<% else %>
|
|
70
|
-
<span class="w-2 h-2 rounded-full bg-red-500 mr-1.5"></span>
|
|
71
|
-
<span class="text-red-600 dark:text-red-400"><%= success_rate %>%</span>
|
|
72
|
-
<% end %>
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
|
|
76
|
-
<!-- Last Executed -->
|
|
77
|
-
<div class="text-gray-400 dark:text-gray-500">
|
|
78
|
-
<% if agent[:last_executed] %>
|
|
79
|
-
<%= time_ago_in_words(agent[:last_executed]) %> ago
|
|
80
|
-
<% else %>
|
|
81
|
-
Never executed
|
|
82
|
-
<% end %>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
<% end %>
|
|
17
|
+
<%= render partial: "rubyllm/agents/agents/agent", locals: { agent: agent } %>
|
|
87
18
|
<% end %>
|
|
88
19
|
</div>
|
|
89
20
|
<% end %>
|
|
@@ -61,8 +61,26 @@
|
|
|
61
61
|
<% end %>
|
|
62
62
|
</div>
|
|
63
63
|
|
|
64
|
-
<div class="text-right
|
|
65
|
-
<p
|
|
64
|
+
<div class="text-right">
|
|
65
|
+
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
66
|
+
<%= number_with_delimiter(@stats[:count]) %> total executions
|
|
67
|
+
</p>
|
|
68
|
+
<div class="flex items-center justify-end gap-3 mt-1">
|
|
69
|
+
<% status_colors = {
|
|
70
|
+
"success" => "bg-green-500",
|
|
71
|
+
"error" => "bg-red-500",
|
|
72
|
+
"timeout" => "bg-yellow-500",
|
|
73
|
+
"running" => "bg-blue-500"
|
|
74
|
+
} %>
|
|
75
|
+
<% @status_distribution.each do |status, count| %>
|
|
76
|
+
<div class="flex items-center gap-1">
|
|
77
|
+
<span class="w-2 h-2 rounded-full <%= status_colors[status] || 'bg-gray-400' %> <%= status == 'running' ? 'animate-pulse' : '' %>"></span>
|
|
78
|
+
<span class="text-xs text-gray-600 dark:text-gray-400">
|
|
79
|
+
<%= number_with_delimiter(count) %>
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
<% end %>
|
|
83
|
+
</div>
|
|
66
84
|
</div>
|
|
67
85
|
</div>
|
|
68
86
|
</div>
|
|
@@ -199,37 +217,6 @@
|
|
|
199
217
|
</div>
|
|
200
218
|
</div>
|
|
201
219
|
|
|
202
|
-
<!-- Status Distribution (compact) -->
|
|
203
|
-
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 mb-6">
|
|
204
|
-
<div class="flex items-center justify-between">
|
|
205
|
-
<p class="text-sm text-gray-500 dark:text-gray-400 uppercase">Status Distribution</p>
|
|
206
|
-
|
|
207
|
-
<div class="flex flex-wrap gap-4">
|
|
208
|
-
<% status_colors = {
|
|
209
|
-
"success" => "#10B981",
|
|
210
|
-
"error" => "#EF4444",
|
|
211
|
-
"timeout" => "#F59E0B",
|
|
212
|
-
"running" => "#3B82F6"
|
|
213
|
-
} %>
|
|
214
|
-
|
|
215
|
-
<% @status_distribution.each do |status, count| %>
|
|
216
|
-
<div class="flex items-center">
|
|
217
|
-
<span
|
|
218
|
-
class="w-2 h-2 rounded-full mr-1.5"
|
|
219
|
-
style="background-color: <%= status_colors[status] || '#6B7280' %>"
|
|
220
|
-
></span>
|
|
221
|
-
|
|
222
|
-
<span class="text-sm text-gray-700 dark:text-gray-300 capitalize"><%= status %></span>
|
|
223
|
-
|
|
224
|
-
<span class="text-sm font-medium text-gray-900 dark:text-gray-100 ml-1">
|
|
225
|
-
(<%= number_with_delimiter(count) %>)
|
|
226
|
-
</span>
|
|
227
|
-
</div>
|
|
228
|
-
<% end %>
|
|
229
|
-
</div>
|
|
230
|
-
</div>
|
|
231
|
-
</div>
|
|
232
|
-
|
|
233
220
|
<!-- Finish Reason Distribution -->
|
|
234
221
|
<% if @finish_reason_distribution.present? && @finish_reason_distribution.any? %>
|
|
235
222
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 mb-6">
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
<div class="space-y-2">
|
|
11
11
|
<% critical_alerts.each do |alert| %>
|
|
12
|
-
<div class="flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg px-4 py-3 shadow-sm">
|
|
12
|
+
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-0 bg-white dark:bg-gray-800 rounded-lg px-4 py-3 shadow-sm">
|
|
13
13
|
<% case alert[:type] %>
|
|
14
14
|
<% when :breaker %>
|
|
15
15
|
<div class="flex items-center">
|
|
16
|
-
<span class="w-2 h-2 bg-orange-500 rounded-full mr-3"></span>
|
|
16
|
+
<span class="w-2 h-2 bg-orange-500 rounded-full mr-3 flex-shrink-0"></span>
|
|
17
17
|
<div>
|
|
18
18
|
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
19
19
|
Circuit breaker open: <%= alert[:data][:agent_type].gsub(/Agent$/, '') %>
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
</p>
|
|
24
24
|
</div>
|
|
25
25
|
</div>
|
|
26
|
-
<span class="text-xs text-orange-600 dark:text-orange-400 font-medium">
|
|
26
|
+
<span class="text-xs text-orange-600 dark:text-orange-400 font-medium ml-5 sm:ml-0">
|
|
27
27
|
<%= alert[:data][:cooldown_remaining] %>s remaining
|
|
28
28
|
</span>
|
|
29
29
|
|
|
30
30
|
<% when :budget_breach %>
|
|
31
31
|
<div class="flex items-center">
|
|
32
|
-
<span class="w-2 h-2 bg-red-500 rounded-full mr-3"></span>
|
|
32
|
+
<span class="w-2 h-2 bg-red-500 rounded-full mr-3 flex-shrink-0"></span>
|
|
33
33
|
<div>
|
|
34
34
|
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
35
35
|
<%= alert[:data][:period].to_s.capitalize %> budget exceeded
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
</p>
|
|
40
40
|
</div>
|
|
41
41
|
</div>
|
|
42
|
-
<%= link_to "Adjust", ruby_llm_agents.settings_path, class: "text-xs text-red-600 dark:text-red-400 hover:underline font-medium" %>
|
|
42
|
+
<%= link_to "Adjust", ruby_llm_agents.settings_path, class: "text-xs text-red-600 dark:text-red-400 hover:underline font-medium ml-5 sm:ml-0" %>
|
|
43
43
|
|
|
44
44
|
<% when :error_spike %>
|
|
45
45
|
<div class="flex items-center">
|
|
46
|
-
<span class="w-2 h-2 bg-red-500 rounded-full mr-3 animate-pulse"></span>
|
|
46
|
+
<span class="w-2 h-2 bg-red-500 rounded-full mr-3 flex-shrink-0 animate-pulse"></span>
|
|
47
47
|
<div>
|
|
48
48
|
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
49
49
|
Error spike detected
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
</p>
|
|
54
54
|
</div>
|
|
55
55
|
</div>
|
|
56
|
-
<%= link_to "View failures", ruby_llm_agents.executions_path(status: "error"), class: "text-xs text-red-600 dark:text-red-400 hover:underline font-medium" %>
|
|
56
|
+
<%= link_to "View failures", ruby_llm_agents.executions_path(status: "error"), class: "text-xs text-red-600 dark:text-red-400 hover:underline font-medium ml-5 sm:ml-0" %>
|
|
57
57
|
<% end %>
|
|
58
58
|
</div>
|
|
59
59
|
<% end %>
|