ruby_llm-agents 0.2.4 → 0.3.1

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +413 -0
  3. data/app/channels/ruby_llm/agents/executions_channel.rb +24 -1
  4. data/app/controllers/concerns/ruby_llm/agents/filterable.rb +81 -0
  5. data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +51 -0
  6. data/app/controllers/ruby_llm/agents/agents_controller.rb +228 -59
  7. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +167 -12
  8. data/app/controllers/ruby_llm/agents/executions_controller.rb +189 -31
  9. data/app/controllers/ruby_llm/agents/settings_controller.rb +20 -0
  10. data/app/helpers/ruby_llm/agents/application_helper.rb +307 -7
  11. data/app/models/ruby_llm/agents/execution/analytics.rb +224 -20
  12. data/app/models/ruby_llm/agents/execution/metrics.rb +41 -25
  13. data/app/models/ruby_llm/agents/execution/scopes.rb +234 -14
  14. data/app/models/ruby_llm/agents/execution.rb +259 -16
  15. data/app/services/ruby_llm/agents/agent_registry.rb +49 -12
  16. data/app/views/layouts/rubyllm/agents/application.html.erb +351 -85
  17. data/app/views/rubyllm/agents/agents/_version_comparison.html.erb +186 -0
  18. data/app/views/rubyllm/agents/agents/show.html.erb +233 -10
  19. data/app/views/rubyllm/agents/dashboard/_action_center.html.erb +62 -0
  20. data/app/views/rubyllm/agents/dashboard/_alerts_feed.html.erb +62 -0
  21. data/app/views/rubyllm/agents/dashboard/_breaker_strip.html.erb +47 -0
  22. data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +165 -0
  23. data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +10 -0
  24. data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +71 -0
  25. data/app/views/rubyllm/agents/dashboard/index.html.erb +215 -109
  26. data/app/views/rubyllm/agents/executions/_filters.html.erb +152 -155
  27. data/app/views/rubyllm/agents/executions/_list.html.erb +103 -12
  28. data/app/views/rubyllm/agents/executions/dry_run.html.erb +149 -0
  29. data/app/views/rubyllm/agents/executions/index.html.erb +17 -72
  30. data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +16 -2
  31. data/app/views/rubyllm/agents/executions/show.html.erb +693 -14
  32. data/app/views/rubyllm/agents/settings/show.html.erb +369 -0
  33. data/app/views/rubyllm/agents/shared/_filter_dropdown.html.erb +121 -0
  34. data/app/views/rubyllm/agents/shared/_select_dropdown.html.erb +85 -0
  35. data/config/routes.rb +7 -0
  36. data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +27 -0
  37. data/lib/generators/ruby_llm_agents/templates/add_caching_migration.rb.tt +23 -0
  38. data/lib/generators/ruby_llm_agents/templates/add_finish_reason_migration.rb.tt +19 -0
  39. data/lib/generators/ruby_llm_agents/templates/add_routing_migration.rb.tt +19 -0
  40. data/lib/generators/ruby_llm_agents/templates/add_streaming_migration.rb.tt +8 -0
  41. data/lib/generators/ruby_llm_agents/templates/add_tracing_migration.rb.tt +34 -0
  42. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +66 -4
  43. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +53 -6
  44. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +143 -8
  45. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +38 -1
  46. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +78 -0
  47. data/lib/ruby_llm/agents/alert_manager.rb +207 -0
  48. data/lib/ruby_llm/agents/attempt_tracker.rb +295 -0
  49. data/lib/ruby_llm/agents/base.rb +597 -112
  50. data/lib/ruby_llm/agents/budget_tracker.rb +360 -0
  51. data/lib/ruby_llm/agents/circuit_breaker.rb +197 -0
  52. data/lib/ruby_llm/agents/configuration.rb +279 -1
  53. data/lib/ruby_llm/agents/engine.rb +58 -6
  54. data/lib/ruby_llm/agents/execution_logger_job.rb +17 -6
  55. data/lib/ruby_llm/agents/inflections.rb +13 -2
  56. data/lib/ruby_llm/agents/instrumentation.rb +538 -87
  57. data/lib/ruby_llm/agents/redactor.rb +130 -0
  58. data/lib/ruby_llm/agents/reliability.rb +185 -0
  59. data/lib/ruby_llm/agents/version.rb +3 -1
  60. data/lib/ruby_llm/agents.rb +52 -0
  61. metadata +41 -2
  62. data/app/controllers/ruby_llm/agents/application_controller.rb +0 -37
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03e22b362e20b0322d49726f9bb22f202d4fa642691f3a3b885d5cc4e7c661cf
4
- data.tar.gz: 327a34b0ef344b4a65edf23fda0360b1e2bcc5f33e43881521418686f2113565
3
+ metadata.gz: c2a8bd149077abc08185f8bc5c59d03323ba6adce25f1feed23dfc35d17376de
4
+ data.tar.gz: 4e9d466a76aa4565a6936a8d9cc7499b4b18aa6efb1e9c40baa0a28c35ca656d
5
5
  SHA512:
6
- metadata.gz: 83bdf11edc73064e92f98b220849e5d29ad62aa79ee853880eda80763c70e4a7aec13129f299df70db6dc3a9d08c102a12ca857ca6557b9b6a56db81430ea38a
7
- data.tar.gz: 8c8f29bb00b446e44d2b15578b823812f72e533976244407549bcf4414db51a8e33676fdbb36ebab41a7f22d2d85a1a63de1d2c0c7e40d5b58ebfad128a9a634
6
+ metadata.gz: 3758ac407012134aab9fcf89f0ad7895b3cb38dd9b385772bb8102cea93a4d9247e9b3083e5eaa7e7f2d1969c4079d1b4286168c00353976af84effa6272ec2b
7
+ data.tar.gz: 3787b8665b33714ac6f4221e7a79753563d4b4553b8868a8dba2d9a1b2d3e310c43c3f27064fb7e0aa1bf9addad865b26626127d7862ed52221637c6b81eaa91
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,12 @@ 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
+ - **🔄 Reliability** - Automatic retries, model fallbacks, and circuit breakers for resilient agents
20
+ - **💵 Budget Controls** - Daily/monthly spending limits with hard and soft enforcement
21
+ - **🔔 Alerts** - Slack, webhook, and custom notifications for budget and circuit breaker events
22
+ - **🔒 PII Redaction** - Automatic sanitization of sensitive data in execution logs
15
23
 
16
24
  ## Requirements
17
25
 
@@ -146,6 +154,123 @@ SearchIntentAgent.call(query: "test", dry_run: true)
146
154
  SearchIntentAgent.call(query: "test", skip_cache: true)
147
155
  ```
148
156
 
157
+ ### Streaming Responses
158
+
159
+ Enable real-time streaming to receive LLM responses as they're generated:
160
+
161
+ ```ruby
162
+ class StreamingAgent < ApplicationAgent
163
+ model "gpt-4o"
164
+ streaming true # Enable streaming for this agent
165
+
166
+ param :prompt, required: true
167
+
168
+ def user_prompt
169
+ prompt
170
+ end
171
+ end
172
+ ```
173
+
174
+ #### Using Streaming with a Block
175
+
176
+ ```ruby
177
+ # Stream responses in real-time
178
+ StreamingAgent.call(prompt: "Write a story") do |chunk|
179
+ print chunk # Process each chunk as it arrives
180
+ end
181
+ ```
182
+
183
+ #### HTTP Streaming with ActionController::Live
184
+
185
+ ```ruby
186
+ class StreamingController < ApplicationController
187
+ include ActionController::Live
188
+
189
+ def stream_response
190
+ response.headers['Content-Type'] = 'text/event-stream'
191
+ response.headers['Cache-Control'] = 'no-cache'
192
+
193
+ StreamingAgent.call(prompt: params[:prompt]) do |chunk|
194
+ response.stream.write "data: #{chunk}\n\n"
195
+ end
196
+ ensure
197
+ response.stream.close
198
+ end
199
+ end
200
+ ```
201
+
202
+ #### Time-to-First-Token Tracking
203
+
204
+ Streaming executions automatically track latency metrics:
205
+
206
+ ```ruby
207
+ execution = RubyLLM::Agents::Execution.last
208
+ execution.streaming? # => true
209
+ execution.time_to_first_token_ms # => 245 (milliseconds to first chunk)
210
+ ```
211
+
212
+ #### Global Streaming Configuration
213
+
214
+ Enable streaming by default for all agents:
215
+
216
+ ```ruby
217
+ # config/initializers/ruby_llm_agents.rb
218
+ RubyLLM::Agents.configure do |config|
219
+ config.default_streaming = true
220
+ end
221
+ ```
222
+
223
+ ### Attachments (Vision & Multimodal)
224
+
225
+ Send images, PDFs, and other files to vision-capable models using the `with:` option:
226
+
227
+ ```ruby
228
+ class VisionAgent < ApplicationAgent
229
+ model "gpt-4o" # Use a vision-capable model
230
+ param :question, required: true
231
+
232
+ def user_prompt
233
+ question
234
+ end
235
+ end
236
+ ```
237
+
238
+ #### Single Attachment
239
+
240
+ ```ruby
241
+ # Local file
242
+ VisionAgent.call(question: "Describe this image", with: "photo.jpg")
243
+
244
+ # URL
245
+ VisionAgent.call(question: "What architecture is shown?", with: "https://example.com/building.jpg")
246
+ ```
247
+
248
+ #### Multiple Attachments
249
+
250
+ ```ruby
251
+ VisionAgent.call(
252
+ question: "Compare these two screenshots",
253
+ with: ["screenshot_v1.png", "screenshot_v2.png"]
254
+ )
255
+ ```
256
+
257
+ #### Supported File Types
258
+
259
+ RubyLLM automatically detects file types:
260
+
261
+ - **Images:** `.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`, `.bmp`
262
+ - **Videos:** `.mp4`, `.mov`, `.avi`, `.webm`
263
+ - **Audio:** `.mp3`, `.wav`, `.m4a`, `.ogg`, `.flac`
264
+ - **Documents:** `.pdf`, `.txt`, `.md`, `.csv`, `.json`, `.xml`
265
+ - **Code:** `.rb`, `.py`, `.js`, `.html`, `.css`, and many others
266
+
267
+ #### Debug Mode with Attachments
268
+
269
+ ```ruby
270
+ VisionAgent.call(question: "test", with: "image.png", dry_run: true)
271
+ # => { ..., attachments: "image.png", ... }
272
+ ```
273
+
149
274
  ## Usage Guide
150
275
 
151
276
  ### Agent DSL
@@ -394,6 +519,269 @@ class RecommendationAgent < ApplicationAgent
394
519
  end
395
520
  ```
396
521
 
522
+ ## Reliability Features
523
+
524
+ RubyLLM::Agents provides built-in reliability features to make your agents resilient against API failures, rate limits, and transient errors.
525
+
526
+ ### Automatic Retries
527
+
528
+ Configure retry behavior for transient failures:
529
+
530
+ ```ruby
531
+ class ReliableAgent < ApplicationAgent
532
+ model "gpt-4o"
533
+
534
+ # Retry up to 3 times with exponential backoff
535
+ retries max: 3, backoff: :exponential, base: 0.5, max_delay: 10.0
536
+
537
+ # Only retry on specific errors (defaults include timeout, network errors)
538
+ retries max: 3, on: [Timeout::Error, Net::ReadTimeout, Faraday::TimeoutError]
539
+
540
+ param :query, required: true
541
+
542
+ def user_prompt
543
+ query
544
+ end
545
+ end
546
+ ```
547
+
548
+ Backoff strategies:
549
+ - `:exponential` - Delay doubles each retry (0.5s, 1s, 2s, 4s...)
550
+ - `:constant` - Same delay each retry
551
+ - Jitter is automatically added to prevent thundering herd
552
+
553
+ ### Model Fallbacks
554
+
555
+ Automatically try alternative models if the primary fails:
556
+
557
+ ```ruby
558
+ class FallbackAgent < ApplicationAgent
559
+ model "gpt-4o"
560
+
561
+ # Try these models in order if primary fails
562
+ fallback_models "gpt-4o-mini", "claude-3-5-sonnet", "gemini-2.0-flash"
563
+
564
+ # Combine with retries
565
+ retries max: 2
566
+ fallback_models "gpt-4o-mini", "claude-3-sonnet"
567
+
568
+ param :query, required: true
569
+
570
+ def user_prompt
571
+ query
572
+ end
573
+ end
574
+ ```
575
+
576
+ The agent will try `gpt-4o` (with 2 retries), then `gpt-4o-mini` (with 2 retries), and so on.
577
+
578
+ ### Circuit Breaker
579
+
580
+ Prevent cascading failures by temporarily blocking requests to failing models:
581
+
582
+ ```ruby
583
+ class ProtectedAgent < ApplicationAgent
584
+ model "gpt-4o"
585
+ fallback_models "claude-3-sonnet"
586
+
587
+ # Open circuit after 10 errors within 60 seconds
588
+ # Keep circuit open for 5 minutes before retrying
589
+ circuit_breaker errors: 10, within: 60, cooldown: 300
590
+
591
+ param :query, required: true
592
+
593
+ def user_prompt
594
+ query
595
+ end
596
+ end
597
+ ```
598
+
599
+ Circuit breaker states:
600
+ - **Closed** - Normal operation, requests pass through
601
+ - **Open** - Model is blocked, requests skip to fallback or fail fast
602
+ - **Half-Open** - After cooldown, one request is allowed to test recovery
603
+
604
+ ### Total Timeout
605
+
606
+ Set a maximum time for the entire operation including all retries:
607
+
608
+ ```ruby
609
+ class TimeBoundAgent < ApplicationAgent
610
+ model "gpt-4o"
611
+ retries max: 5
612
+ fallback_models "gpt-4o-mini"
613
+
614
+ # Abort everything after 30 seconds total
615
+ total_timeout 30
616
+
617
+ param :query, required: true
618
+
619
+ def user_prompt
620
+ query
621
+ end
622
+ end
623
+ ```
624
+
625
+ ### Viewing Attempt Details
626
+
627
+ When reliability features are enabled, the dashboard shows all attempts:
628
+
629
+ ```ruby
630
+ execution = RubyLLM::Agents::Execution.last
631
+
632
+ # Check if retries/fallbacks were used
633
+ execution.has_retries? # => true
634
+ execution.used_fallback? # => true
635
+ execution.attempts_count # => 3
636
+
637
+ # Get attempt details
638
+ execution.attempts.each do |attempt|
639
+ puts "Model: #{attempt['model_id']}"
640
+ puts "Duration: #{attempt['duration_ms']}ms"
641
+ puts "Error: #{attempt['error_class']}" if attempt['error_class']
642
+ puts "Short-circuited: #{attempt['short_circuited']}"
643
+ end
644
+
645
+ # Find the successful attempt
646
+ execution.successful_attempt # => Hash with attempt data
647
+ execution.chosen_model_id # => "claude-3-sonnet" (the model that succeeded)
648
+ ```
649
+
650
+ ## Governance & Cost Controls
651
+
652
+ ### Budget Limits
653
+
654
+ Set spending limits at global and per-agent levels:
655
+
656
+ ```ruby
657
+ # config/initializers/ruby_llm_agents.rb
658
+ RubyLLM::Agents.configure do |config|
659
+ config.budgets = {
660
+ # Global limits apply to all agents combined
661
+ global_daily: 100.0, # $100/day across all agents
662
+ global_monthly: 2000.0, # $2000/month across all agents
663
+
664
+ # Per-agent limits
665
+ per_agent_daily: {
666
+ "ExpensiveAgent" => 50.0, # $50/day for this agent
667
+ "CheapAgent" => 5.0 # $5/day for this agent
668
+ },
669
+ per_agent_monthly: {
670
+ "ExpensiveAgent" => 500.0
671
+ },
672
+
673
+ # Enforcement mode
674
+ # :hard - Block requests when budget exceeded
675
+ # :soft - Allow requests but log warnings
676
+ enforcement: :hard
677
+ }
678
+ end
679
+ ```
680
+
681
+ Querying budget status:
682
+
683
+ ```ruby
684
+ # Get current budget status
685
+ status = RubyLLM::Agents::BudgetTracker.status(agent_type: "MyAgent")
686
+ # => {
687
+ # global_daily: { limit: 100.0, current: 45.50, remaining: 54.50, percentage_used: 45.5 },
688
+ # global_monthly: { limit: 2000.0, current: 890.0, remaining: 1110.0, percentage_used: 44.5 }
689
+ # }
690
+
691
+ # Check remaining budget
692
+ RubyLLM::Agents::BudgetTracker.remaining_budget(:global, :daily)
693
+ # => 54.50
694
+ ```
695
+
696
+ ### Alerts
697
+
698
+ Get notified when important events occur:
699
+
700
+ ```ruby
701
+ # config/initializers/ruby_llm_agents.rb
702
+ RubyLLM::Agents.configure do |config|
703
+ config.alerts = {
704
+ # Events to alert on
705
+ on_events: [
706
+ :budget_soft_cap, # Budget threshold reached (configurable %)
707
+ :budget_hard_cap, # Budget exceeded (with hard enforcement)
708
+ :breaker_open # Circuit breaker opened
709
+ ],
710
+
711
+ # Slack webhook
712
+ slack_webhook_url: ENV['SLACK_WEBHOOK_URL'],
713
+
714
+ # Generic webhook (receives JSON payload)
715
+ webhook_url: "https://your-app.com/webhooks/llm-alerts",
716
+
717
+ # Custom handler
718
+ custom: ->(event, payload) {
719
+ # event: :budget_hard_cap
720
+ # payload: { scope: :global_daily, limit: 100.0, current: 105.0 }
721
+
722
+ MyNotificationService.notify(
723
+ title: "LLM Budget Alert",
724
+ message: "#{event}: #{payload}"
725
+ )
726
+ }
727
+ }
728
+ end
729
+ ```
730
+
731
+ Alert payload examples:
732
+
733
+ ```ruby
734
+ # Budget alert
735
+ {
736
+ event: :budget_hard_cap,
737
+ scope: :global_daily,
738
+ limit: 100.0,
739
+ current: 105.50,
740
+ agent_type: "ExpensiveAgent"
741
+ }
742
+
743
+ # Circuit breaker alert
744
+ {
745
+ event: :breaker_open,
746
+ agent_type: "MyAgent",
747
+ model_id: "gpt-4o",
748
+ failure_count: 10,
749
+ window_seconds: 60
750
+ }
751
+ ```
752
+
753
+ ### PII Redaction
754
+
755
+ Automatically redact sensitive data from execution logs:
756
+
757
+ ```ruby
758
+ # config/initializers/ruby_llm_agents.rb
759
+ RubyLLM::Agents.configure do |config|
760
+ config.redaction = {
761
+ # Fields to redact (applied to parameters)
762
+ # Default: password, token, api_key, secret, credential, auth, key, access_token
763
+ fields: %w[ssn credit_card phone_number],
764
+
765
+ # Regex patterns to redact from prompts/responses
766
+ patterns: [
767
+ /\b\d{3}-\d{2}-\d{4}\b/, # SSN
768
+ /\b\d{16}\b/, # Credit card
769
+ /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i # Email
770
+ ],
771
+
772
+ # Replacement text
773
+ placeholder: "[REDACTED]",
774
+
775
+ # Truncate long values
776
+ max_value_length: 1000
777
+ }
778
+
779
+ # Control what gets persisted
780
+ config.persist_prompts = true # Store system/user prompts
781
+ config.persist_responses = true # Store LLM responses
782
+ end
783
+ ```
784
+
397
785
  ## Configuration
398
786
 
399
787
  Edit `config/initializers/ruby_llm_agents.rb`:
@@ -413,6 +801,9 @@ RubyLLM::Agents.configure do |config|
413
801
  # Default timeout for LLM requests (in seconds)
414
802
  config.default_timeout = 60
415
803
 
804
+ # Enable streaming by default for all agents
805
+ config.default_streaming = false
806
+
416
807
  # ============================================================================
417
808
  # Caching Configuration
418
809
  # ============================================================================
@@ -599,6 +990,18 @@ trend = RubyLLM::Agents::Execution.trend_analysis(
599
990
  # ]
600
991
  ```
601
992
 
993
+ ### Streaming Analytics
994
+
995
+ ```ruby
996
+ # Percentage of executions using streaming
997
+ RubyLLM::Agents::Execution.streaming_rate
998
+ # => 45.5
999
+
1000
+ # Average time-to-first-token for streaming executions (milliseconds)
1001
+ RubyLLM::Agents::Execution.avg_time_to_first_token
1002
+ # => 245.3
1003
+ ```
1004
+
602
1005
  ### Scopes
603
1006
 
604
1007
  Chain scopes for complex queries:
@@ -652,6 +1055,10 @@ expensive_slow_failures = RubyLLM::Agents::Execution
652
1055
 
653
1056
  # Token usage
654
1057
  .high_token_usage(threshold)
1058
+
1059
+ # Streaming
1060
+ .streaming
1061
+ .non_streaming
655
1062
  ```
656
1063
 
657
1064
  ## Generators
@@ -689,8 +1096,14 @@ rails generate ruby_llm_agents:install
689
1096
  ```bash
690
1097
  # Upgrade to latest schema (when gem is updated)
691
1098
  rails generate ruby_llm_agents:upgrade
1099
+ rails db:migrate
692
1100
  ```
693
1101
 
1102
+ This creates migrations for new features like:
1103
+ - `system_prompt` and `user_prompt` columns for prompt persistence
1104
+ - `attempts` JSONB column for reliability tracking
1105
+ - `chosen_model_id` for fallback model tracking
1106
+
694
1107
  ## Background Jobs
695
1108
 
696
1109
  For production environments, enable async logging:
@@ -7,14 +7,37 @@ module RubyLLM
7
7
  # Broadcasts execution create/update events to subscribed clients.
8
8
  # Used by the dashboard to show live execution status changes.
9
9
  #
10
- # Inherits from the host app's ApplicationCable::Channel (note the :: prefix)
10
+ # Inherits from the host app's ApplicationCable::Channel (note the :: prefix
11
+ # to reference the root namespace, not the engine's namespace).
11
12
  #
13
+ # @example JavaScript subscription
14
+ # import { createConsumer } from "@rails/actioncable"
15
+ # const consumer = createConsumer()
16
+ # consumer.subscriptions.create("RubyLLM::Agents::ExecutionsChannel", {
17
+ # received(data) {
18
+ # console.log("Execution update:", data)
19
+ # }
20
+ # })
21
+ #
22
+ # @see Execution#broadcast_execution Broadcast trigger
23
+ # @api private
12
24
  class ExecutionsChannel < ::ApplicationCable::Channel
25
+ # Subscribes the client to the executions broadcast stream
26
+ #
27
+ # Called automatically when a client connects to this channel.
28
+ # Streams from the "ruby_llm_agents:executions" channel name.
29
+ #
30
+ # @return [void]
13
31
  def subscribed
14
32
  stream_from "ruby_llm_agents:executions"
15
33
  logger.info "[RubyLLM::Agents] Client subscribed to executions channel"
16
34
  end
17
35
 
36
+ # Cleans up when a client disconnects
37
+ #
38
+ # Called automatically when the WebSocket connection is closed.
39
+ #
40
+ # @return [void]
18
41
  def unsubscribed
19
42
  logger.info "[RubyLLM::Agents] Client unsubscribed from executions channel"
20
43
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Controller concern for parsing and applying filters
6
+ #
7
+ # Provides methods for parsing filter parameters from requests
8
+ # and applying them to ActiveRecord scopes.
9
+ #
10
+ # @example Including in a controller
11
+ # class ExecutionsController < ApplicationController
12
+ # include Filterable
13
+ #
14
+ # def index
15
+ # statuses = parse_array_param(:statuses)
16
+ # @scope = apply_status_filter(Execution.all, statuses)
17
+ # end
18
+ # end
19
+ #
20
+ # @api private
21
+ module Filterable
22
+ extend ActiveSupport::Concern
23
+
24
+ # Valid status values for filtering
25
+ VALID_STATUSES = %w[running success error timeout].freeze
26
+
27
+ private
28
+
29
+ # Parses an array parameter from the request
30
+ #
31
+ # Handles both array format (?key[]=a&key[]=b) and
32
+ # comma-separated format (?key=a,b).
33
+ #
34
+ # @param key [Symbol] The parameter key
35
+ # @return [Array<String>] Parsed values (empty if blank)
36
+ def parse_array_param(key)
37
+ value = params[key]
38
+ return [] if value.blank?
39
+
40
+ (value.is_a?(Array) ? value : value.to_s.split(",")).select(&:present?)
41
+ end
42
+
43
+ # Parses the days parameter for time filtering
44
+ #
45
+ # @return [Integer, nil] Number of days or nil if invalid/missing
46
+ def parse_days_param
47
+ return nil unless params[:days].present?
48
+
49
+ days = params[:days].to_i
50
+ days.positive? ? days : nil
51
+ end
52
+
53
+ # Filters status values to only valid ones
54
+ #
55
+ # @param statuses [Array<String>] Status values to validate
56
+ # @return [Array<String>] Valid status values only
57
+ def validate_statuses(statuses)
58
+ statuses.select { |s| VALID_STATUSES.include?(s) }
59
+ end
60
+
61
+ # Applies status filter to a scope
62
+ #
63
+ # @param scope [ActiveRecord::Relation] The base scope
64
+ # @param statuses [Array<String>] Status values to filter by
65
+ # @return [ActiveRecord::Relation] Filtered scope
66
+ def apply_status_filter(scope, statuses)
67
+ valid_statuses = validate_statuses(statuses)
68
+ valid_statuses.any? ? scope.where(status: valid_statuses) : scope
69
+ end
70
+
71
+ # Applies time filter to a scope
72
+ #
73
+ # @param scope [ActiveRecord::Relation] The base scope
74
+ # @param days [Integer, nil] Number of days to filter by
75
+ # @return [ActiveRecord::Relation] Filtered scope
76
+ def apply_time_filter(scope, days)
77
+ days.present? && days.positive? ? scope.where("created_at >= ?", days.days.ago) : scope
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Agents
5
+ # Controller concern for pagination
6
+ #
7
+ # Provides simple offset-based pagination with consistent return format.
8
+ #
9
+ # @example Using in a controller
10
+ # result = paginate(Execution.all)
11
+ # @executions = result[:records]
12
+ # @pagination = result[:pagination]
13
+ #
14
+ # @api private
15
+ module Paginatable
16
+ extend ActiveSupport::Concern
17
+
18
+ private
19
+
20
+ # Paginates a scope with optional ordering
21
+ #
22
+ # @param scope [ActiveRecord::Relation] The scope to paginate
23
+ # @param ordered [Boolean] Whether to apply default descending order (default: true)
24
+ # @return [Hash] Contains :records and :pagination keys
25
+ # @option return [ActiveRecord::Relation] :records Paginated records
26
+ # @option return [Hash] :pagination Pagination metadata
27
+ # - :current_page [Integer] Current page number
28
+ # - :per_page [Integer] Records per page
29
+ # - :total_count [Integer] Total record count
30
+ # - :total_pages [Integer] Total page count
31
+ def paginate(scope, ordered: true)
32
+ page = [(params[:page] || 1).to_i, 1].max
33
+ per_page = RubyLLM::Agents.configuration.per_page
34
+ offset = (page - 1) * per_page
35
+
36
+ scope = scope.order(created_at: :desc) if ordered
37
+ total_count = scope.count
38
+
39
+ {
40
+ records: scope.offset(offset).limit(per_page),
41
+ pagination: {
42
+ current_page: page,
43
+ per_page: per_page,
44
+ total_count: total_count,
45
+ total_pages: (total_count.to_f / per_page).ceil
46
+ }
47
+ }
48
+ end
49
+ end
50
+ end
51
+ end