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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d0cf7b20ca960eab7d26da1c568c5aa5078e12e887a4d93c9b472d6897cf1e5e
4
- data.tar.gz: 59372d2c80bd8fdb23d49ffdd672911948bf1b78da8243aaac7df926a378bd59
3
+ metadata.gz: 26b04f0b8d00a9c87b5555beccacbfc83208be0a370b0fcc6e7ee518c2543282
4
+ data.tar.gz: 51a1d674af7e489ad6020eb324b13b6509137e035c45bb53d140ca22574fc187
5
5
  SHA512:
6
- metadata.gz: 1902ad245d20d405fe69633e122a83302bbd5b95e5b65fe0d24b1da14b0effaf27e9308e4a5a42bd9072545b2d128ed173214812667727fb2ea0de43948f1ee1
7
- data.tar.gz: d3937f03bc62481c6257c55c9a7f1a8c6eccc9fc08a759c8f40334b21000a12cc67e5277ca0d018b551c3abb6b077d4ffc1caa8bfaa3f2d8e784aac98c1e9bf8
6
+ metadata.gz: 69635d4258f0082742d1b899e26e21019020e58f904df3a20833c7d7035dc39f139a4d07568a598735b4071e694dff48cf7ca67b37f04ae3f6341d9c32e30f8c
7
+ data.tar.gz: f655a50e12ce7117f34bf7a1d0cbc3c8b7168233c6fd4ebcd63e5678de92b0d5fad61152f4001a173b201673dcd87380f4a22faa9b6d542210428e3896b7482d
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # RubyLLM::Agents
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/ruby_llm-agents.svg)](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
- <%= 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 %>
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 text-sm text-gray-500 dark:text-gray-400">
65
- <p><%= @stats[:count] %> total executions</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 %>