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.
- checksums.yaml +4 -4
- data/README.md +413 -0
- data/app/channels/ruby_llm/agents/executions_channel.rb +24 -1
- data/app/controllers/concerns/ruby_llm/agents/filterable.rb +81 -0
- data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +51 -0
- data/app/controllers/ruby_llm/agents/agents_controller.rb +228 -59
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +167 -12
- data/app/controllers/ruby_llm/agents/executions_controller.rb +189 -31
- data/app/controllers/ruby_llm/agents/settings_controller.rb +20 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +307 -7
- data/app/models/ruby_llm/agents/execution/analytics.rb +224 -20
- data/app/models/ruby_llm/agents/execution/metrics.rb +41 -25
- data/app/models/ruby_llm/agents/execution/scopes.rb +234 -14
- data/app/models/ruby_llm/agents/execution.rb +259 -16
- data/app/services/ruby_llm/agents/agent_registry.rb +49 -12
- data/app/views/layouts/rubyllm/agents/application.html.erb +351 -85
- data/app/views/rubyllm/agents/agents/_version_comparison.html.erb +186 -0
- data/app/views/rubyllm/agents/agents/show.html.erb +233 -10
- data/app/views/rubyllm/agents/dashboard/_action_center.html.erb +62 -0
- data/app/views/rubyllm/agents/dashboard/_alerts_feed.html.erb +62 -0
- data/app/views/rubyllm/agents/dashboard/_breaker_strip.html.erb +47 -0
- data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +165 -0
- data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +10 -0
- data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +71 -0
- data/app/views/rubyllm/agents/dashboard/index.html.erb +215 -109
- data/app/views/rubyllm/agents/executions/_filters.html.erb +152 -155
- data/app/views/rubyllm/agents/executions/_list.html.erb +103 -12
- data/app/views/rubyllm/agents/executions/dry_run.html.erb +149 -0
- data/app/views/rubyllm/agents/executions/index.html.erb +17 -72
- data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +16 -2
- data/app/views/rubyllm/agents/executions/show.html.erb +693 -14
- data/app/views/rubyllm/agents/settings/show.html.erb +369 -0
- data/app/views/rubyllm/agents/shared/_filter_dropdown.html.erb +121 -0
- data/app/views/rubyllm/agents/shared/_select_dropdown.html.erb +85 -0
- data/config/routes.rb +7 -0
- data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +27 -0
- data/lib/generators/ruby_llm_agents/templates/add_caching_migration.rb.tt +23 -0
- data/lib/generators/ruby_llm_agents/templates/add_finish_reason_migration.rb.tt +19 -0
- data/lib/generators/ruby_llm_agents/templates/add_routing_migration.rb.tt +19 -0
- data/lib/generators/ruby_llm_agents/templates/add_streaming_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm_agents/templates/add_tracing_migration.rb.tt +34 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +66 -4
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +53 -6
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +143 -8
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +38 -1
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +78 -0
- data/lib/ruby_llm/agents/alert_manager.rb +207 -0
- data/lib/ruby_llm/agents/attempt_tracker.rb +295 -0
- data/lib/ruby_llm/agents/base.rb +597 -112
- data/lib/ruby_llm/agents/budget_tracker.rb +360 -0
- data/lib/ruby_llm/agents/circuit_breaker.rb +197 -0
- data/lib/ruby_llm/agents/configuration.rb +279 -1
- data/lib/ruby_llm/agents/engine.rb +58 -6
- data/lib/ruby_llm/agents/execution_logger_job.rb +17 -6
- data/lib/ruby_llm/agents/inflections.rb +13 -2
- data/lib/ruby_llm/agents/instrumentation.rb +538 -87
- data/lib/ruby_llm/agents/redactor.rb +130 -0
- data/lib/ruby_llm/agents/reliability.rb +185 -0
- data/lib/ruby_llm/agents/version.rb +3 -1
- data/lib/ruby_llm/agents.rb +52 -0
- metadata +41 -2
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c2a8bd149077abc08185f8bc5c59d03323ba6adce25f1feed23dfc35d17376de
|
|
4
|
+
data.tar.gz: 4e9d466a76aa4565a6936a8d9cc7499b4b18aa6efb1e9c40baa0a28c35ca656d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3758ac407012134aab9fcf89f0ad7895b3cb38dd9b385772bb8102cea93a4d9247e9b3083e5eaa7e7f2d1969c4079d1b4286168c00353976af84effa6272ec2b
|
|
7
|
+
data.tar.gz: 3787b8665b33714ac6f4221e7a79753563d4b4553b8868a8dba2d9a1b2d3e310c43c3f27064fb7e0aa1bf9addad865b26626127d7862ed52221637c6b81eaa91
|
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,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
|