ruby_llm-agents 0.3.3 → 0.3.5
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 +132 -1263
- data/app/controllers/concerns/ruby_llm/agents/filterable.rb +5 -1
- data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +2 -1
- data/app/controllers/ruby_llm/agents/agents_controller.rb +21 -2
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +137 -9
- data/app/controllers/ruby_llm/agents/executions_controller.rb +83 -5
- data/app/models/ruby_llm/agents/execution/analytics.rb +103 -12
- data/app/models/ruby_llm/agents/execution/scopes.rb +25 -0
- data/app/models/ruby_llm/agents/execution/workflow.rb +299 -0
- data/app/models/ruby_llm/agents/execution.rb +28 -59
- data/app/models/ruby_llm/agents/tenant_budget.rb +165 -0
- data/app/services/ruby_llm/agents/agent_registry.rb +118 -7
- data/app/views/layouts/ruby_llm/agents/application.html.erb +430 -0
- data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +23 -0
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +125 -0
- data/app/views/ruby_llm/agents/agents/index.html.erb +93 -0
- data/app/views/ruby_llm/agents/agents/show.html.erb +775 -0
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +112 -0
- data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +75 -0
- data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_execution_item.html.erb +7 -4
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +84 -0
- data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +115 -0
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +49 -0
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +155 -0
- data/app/views/{rubyllm → ruby_llm}/agents/executions/_execution.html.erb +1 -1
- data/app/views/{rubyllm → ruby_llm}/agents/executions/_filters.html.erb +39 -11
- data/app/views/{rubyllm → ruby_llm}/agents/executions/_list.html.erb +19 -9
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +101 -0
- data/app/views/ruby_llm/agents/executions/index.html.erb +88 -0
- data/app/views/{rubyllm → ruby_llm}/agents/executions/show.html.erb +260 -200
- data/app/views/{rubyllm → ruby_llm}/agents/settings/show.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +48 -0
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +251 -0
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_filter_dropdown.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +27 -0
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_select_dropdown.html.erb +1 -1
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_badge.html.erb +1 -1
- data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_dot.html.erb +1 -1
- data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +26 -0
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +61 -0
- data/config/routes.rb +2 -0
- data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +97 -0
- data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +3 -3
- data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +23 -0
- data/lib/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt +2 -2
- data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +38 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +45 -0
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +17 -5
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +13 -0
- data/lib/ruby_llm/agents/alert_manager.rb +20 -16
- data/lib/ruby_llm/agents/base/caching.rb +40 -0
- data/lib/ruby_llm/agents/base/cost_calculation.rb +105 -0
- data/lib/ruby_llm/agents/base/dsl.rb +261 -0
- data/lib/ruby_llm/agents/base/execution.rb +258 -0
- data/lib/ruby_llm/agents/base/reliability_execution.rb +136 -0
- data/lib/ruby_llm/agents/base/response_building.rb +86 -0
- data/lib/ruby_llm/agents/base/tool_tracking.rb +57 -0
- data/lib/ruby_llm/agents/base.rb +37 -801
- data/lib/ruby_llm/agents/budget_tracker.rb +250 -139
- data/lib/ruby_llm/agents/cache_helper.rb +98 -0
- data/lib/ruby_llm/agents/circuit_breaker.rb +48 -30
- data/lib/ruby_llm/agents/configuration.rb +40 -1
- data/lib/ruby_llm/agents/engine.rb +65 -1
- data/lib/ruby_llm/agents/inflections.rb +14 -0
- data/lib/ruby_llm/agents/instrumentation.rb +66 -0
- data/lib/ruby_llm/agents/reliability.rb +8 -2
- data/lib/ruby_llm/agents/version.rb +1 -1
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +254 -0
- data/lib/ruby_llm/agents/workflow/parallel.rb +282 -0
- data/lib/ruby_llm/agents/workflow/pipeline.rb +306 -0
- data/lib/ruby_llm/agents/workflow/result.rb +390 -0
- data/lib/ruby_llm/agents/workflow/router.rb +429 -0
- data/lib/ruby_llm/agents/workflow.rb +232 -0
- data/lib/ruby_llm/agents.rb +1 -0
- metadata +57 -75
- data/app/channels/ruby_llm/agents/executions_channel.rb +0 -46
- data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +0 -56
- data/app/javascript/ruby_llm/agents/controllers/index.js +0 -12
- data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +0 -83
- data/app/views/layouts/rubyllm/agents/application.html.erb +0 -626
- data/app/views/rubyllm/agents/agents/index.html.erb +0 -20
- data/app/views/rubyllm/agents/agents/show.html.erb +0 -772
- data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +0 -165
- data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +0 -10
- data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +0 -71
- data/app/views/rubyllm/agents/dashboard/index.html.erb +0 -197
- data/app/views/rubyllm/agents/executions/index.html.erb +0 -28
- data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +0 -18
- data/app/views/rubyllm/agents/shared/_executions_table.html.erb +0 -193
- /data/app/views/{rubyllm → ruby_llm}/agents/agents/_agent.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/agents/_version_comparison.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_action_center.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_alerts_feed.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_breaker_strip.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/executions/dry_run.html.erb +0 -0
- /data/app/views/{rubyllm → ruby_llm}/agents/shared/_stat_card.html.erb +0 -0
data/README.md
CHANGED
|
@@ -1,657 +1,152 @@
|
|
|
1
1
|
# RubyLLM::Agents
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Production-ready Rails engine for building, managing, and monitoring LLM-powered AI agents**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://rubygems.org/gems/ruby_llm-agents)
|
|
6
|
+
[](https://www.ruby-lang.org)
|
|
7
|
+
[](https://rubyonrails.org)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://github.com/adham90/ruby_llm-agents/wiki)
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **🤖 Agent DSL** - Declarative configuration for LLM agents with model, temperature, parameters, and caching
|
|
10
|
-
- **📊 Execution Tracking** - Automatic logging of all agent executions with token usage and costs
|
|
11
|
-
- **💰 Cost Analytics** - Track spending by agent, model, and time period with detailed breakdowns
|
|
12
|
-
- **📈 Dashboard UI** - Beautiful Turbo-powered dashboard for monitoring agents
|
|
13
|
-
- **⚡ Performance** - Built-in caching with configurable TTL and cache key versioning
|
|
14
|
-
- **🛠️ Generators** - Quickly scaffold new agents with customizable templates
|
|
15
|
-
- **🔍 Anomaly Detection** - Automatic warnings for unusual cost or duration patterns
|
|
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
|
|
20
|
-
- **🔄 Reliability** - Automatic retries, model fallbacks, and circuit breakers for resilient agents
|
|
21
|
-
- **💵 Budget Controls** - Daily/monthly spending limits with hard and soft enforcement
|
|
22
|
-
- **🔔 Alerts** - Slack, webhook, and custom notifications for budget and circuit breaker events
|
|
23
|
-
- **🔒 PII Redaction** - Automatic sanitization of sensitive data in execution logs
|
|
24
|
-
|
|
25
|
-
## Requirements
|
|
11
|
+
Build intelligent AI agents in Ruby with a clean DSL, automatic execution tracking, cost analytics, budget controls, and a beautiful real-time dashboard. Supports **OpenAI GPT-4**, **Anthropic Claude**, **Google Gemini**, and more through [RubyLLM](https://github.com/crmne/ruby_llm).
|
|
26
12
|
|
|
27
|
-
|
|
28
|
-
- **Rails**: >= 7.0
|
|
13
|
+
## Why RubyLLM::Agents?
|
|
29
14
|
|
|
30
|
-
|
|
15
|
+
- **Rails-Native** - Seamlessly integrates with your Rails app: models, jobs, caching, and Hotwire
|
|
16
|
+
- **Production-Ready** - Built-in retries, model fallbacks, circuit breakers, and budget limits
|
|
17
|
+
- **Full Observability** - Track every execution with costs, tokens, duration, and errors
|
|
18
|
+
- **Workflow Orchestration** - Compose agents into pipelines, parallel tasks, and conditional routers
|
|
19
|
+
- **Zero Lock-in** - Works with any LLM provider supported by RubyLLM
|
|
31
20
|
|
|
32
|
-
|
|
21
|
+
## Features
|
|
33
22
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
23
|
+
| Feature | Description | Docs |
|
|
24
|
+
|---------|-------------|------|
|
|
25
|
+
| **Agent DSL** | Declarative configuration with model, temperature, parameters | [Agent DSL](https://github.com/adham90/ruby_llm-agents/wiki/Agent-DSL) |
|
|
26
|
+
| **Execution Tracking** | Automatic logging with token usage and cost analytics | [Tracking](https://github.com/adham90/ruby_llm-agents/wiki/Execution-Tracking) |
|
|
27
|
+
| **Cost Analytics** | Track spending by agent, model, and time period | [Analytics](https://github.com/adham90/ruby_llm-agents/wiki/Execution-Tracking) |
|
|
28
|
+
| **Reliability** | Automatic retries, model fallbacks, circuit breakers | [Reliability](https://github.com/adham90/ruby_llm-agents/wiki/Reliability) |
|
|
29
|
+
| **Budget Controls** | Daily/monthly limits with hard and soft enforcement | [Budgets](https://github.com/adham90/ruby_llm-agents/wiki/Budget-Controls) |
|
|
30
|
+
| **Workflows** | Pipelines, parallel execution, conditional routers | [Workflows](https://github.com/adham90/ruby_llm-agents/wiki/Workflows) |
|
|
31
|
+
| **Dashboard** | Real-time Turbo-powered monitoring UI | [Dashboard](https://github.com/adham90/ruby_llm-agents/wiki/Dashboard) |
|
|
32
|
+
| **Streaming** | Real-time response streaming with TTFT tracking | [Streaming](https://github.com/adham90/ruby_llm-agents/wiki/Streaming) |
|
|
33
|
+
| **Conversation History** | Multi-turn conversations with message history | [Conversation History](https://github.com/adham90/ruby_llm-agents/wiki/Conversation-History) |
|
|
34
|
+
| **Attachments** | Images, PDFs, and multimodal support | [Attachments](https://github.com/adham90/ruby_llm-agents/wiki/Attachments) |
|
|
35
|
+
| **PII Redaction** | Automatic sensitive data protection | [Security](https://github.com/adham90/ruby_llm-agents/wiki/PII-Redaction) |
|
|
36
|
+
| **Alerts** | Slack, webhook, and custom notifications | [Alerts](https://github.com/adham90/ruby_llm-agents/wiki/Alerts) |
|
|
41
37
|
|
|
42
|
-
##
|
|
38
|
+
## Quick Start
|
|
43
39
|
|
|
44
|
-
###
|
|
40
|
+
### Installation
|
|
45
41
|
|
|
46
42
|
```ruby
|
|
43
|
+
# Gemfile
|
|
47
44
|
gem "ruby_llm-agents"
|
|
48
45
|
```
|
|
49
46
|
|
|
50
|
-
Then run:
|
|
51
|
-
|
|
52
47
|
```bash
|
|
53
48
|
bundle install
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### 2. Run the install generator
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
49
|
rails generate ruby_llm_agents:install
|
|
60
50
|
rails db:migrate
|
|
61
51
|
```
|
|
62
52
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
- Create the `ruby_llm_agents_executions` table for execution tracking
|
|
66
|
-
- Add an initializer at `config/initializers/ruby_llm_agents.rb`
|
|
67
|
-
- Create `app/agents/application_agent.rb` as the base class for your agents
|
|
68
|
-
- Mount the dashboard at `/agents` in your routes
|
|
69
|
-
|
|
70
|
-
### 3. Configure your LLM provider
|
|
71
|
-
|
|
72
|
-
Set up your API keys for the LLM providers you want to use:
|
|
53
|
+
### Configure API Keys
|
|
73
54
|
|
|
74
55
|
```bash
|
|
75
|
-
# .env
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
56
|
+
# .env
|
|
57
|
+
OPENAI_API_KEY=sk-...
|
|
58
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
59
|
+
GOOGLE_API_KEY=...
|
|
79
60
|
```
|
|
80
61
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
### Creating Your First Agent
|
|
84
|
-
|
|
85
|
-
Use the generator to create a new agent:
|
|
62
|
+
### Create Your First Agent
|
|
86
63
|
|
|
87
64
|
```bash
|
|
88
|
-
rails generate ruby_llm_agents:agent SearchIntent query:required
|
|
65
|
+
rails generate ruby_llm_agents:agent SearchIntent query:required
|
|
89
66
|
```
|
|
90
67
|
|
|
91
|
-
This creates `app/agents/search_intent_agent.rb`:
|
|
92
|
-
|
|
93
68
|
```ruby
|
|
69
|
+
# app/agents/search_intent_agent.rb
|
|
94
70
|
class SearchIntentAgent < ApplicationAgent
|
|
95
|
-
model "
|
|
71
|
+
model "gpt-4o"
|
|
96
72
|
temperature 0.0
|
|
97
|
-
version "1.0"
|
|
98
73
|
|
|
99
74
|
param :query, required: true
|
|
100
|
-
param :limit, default: 10
|
|
101
|
-
|
|
102
|
-
private
|
|
103
|
-
|
|
104
|
-
def system_prompt
|
|
105
|
-
<<~PROMPT
|
|
106
|
-
You are a search assistant that parses user queries
|
|
107
|
-
and extracts structured search filters.
|
|
108
|
-
PROMPT
|
|
109
|
-
end
|
|
110
75
|
|
|
111
76
|
def user_prompt
|
|
112
|
-
query
|
|
77
|
+
"Extract search intent from: #{query}"
|
|
113
78
|
end
|
|
114
79
|
|
|
115
80
|
def schema
|
|
116
81
|
@schema ||= RubyLLM::Schema.create do
|
|
117
|
-
string :refined_query, description: "Cleaned
|
|
118
|
-
array :filters, of: :string, description: "Extracted
|
|
119
|
-
integer :category_id, description: "Detected product category", nullable: true
|
|
82
|
+
string :refined_query, description: "Cleaned search query"
|
|
83
|
+
array :filters, of: :string, description: "Extracted filters"
|
|
120
84
|
end
|
|
121
85
|
end
|
|
122
86
|
end
|
|
123
87
|
```
|
|
124
88
|
|
|
125
|
-
###
|
|
89
|
+
### Call the Agent
|
|
126
90
|
|
|
127
91
|
```ruby
|
|
128
|
-
# Basic call
|
|
129
92
|
result = SearchIntentAgent.call(query: "red summer dress under $50")
|
|
130
|
-
# => {
|
|
131
|
-
# refined_query: "red summer dress",
|
|
132
|
-
# filters: ["color:red", "season:summer", "price:<50"],
|
|
133
|
-
# category_id: 42
|
|
134
|
-
# }
|
|
135
|
-
|
|
136
|
-
# With custom parameters
|
|
137
|
-
result = SearchIntentAgent.call(
|
|
138
|
-
query: "blue jeans",
|
|
139
|
-
limit: 20
|
|
140
|
-
)
|
|
141
93
|
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
# =>
|
|
145
|
-
#
|
|
146
|
-
# agent: "SearchIntentAgent",
|
|
147
|
-
# model: "gemini-2.0-flash",
|
|
148
|
-
# temperature: 0.0,
|
|
149
|
-
# system_prompt: "You are a search assistant...",
|
|
150
|
-
# user_prompt: "test",
|
|
151
|
-
# schema: "RubyLLM::Schema"
|
|
152
|
-
# }
|
|
153
|
-
|
|
154
|
-
# Skip cache
|
|
155
|
-
SearchIntentAgent.call(query: "test", skip_cache: true)
|
|
94
|
+
result.content # => { refined_query: "red dress", filters: ["color:red", "price:<50"] }
|
|
95
|
+
result.total_cost # => 0.00025
|
|
96
|
+
result.total_tokens # => 150
|
|
97
|
+
result.duration_ms # => 850
|
|
156
98
|
```
|
|
157
99
|
|
|
158
|
-
###
|
|
100
|
+
### Multi-Turn Conversations
|
|
159
101
|
|
|
160
|
-
|
|
102
|
+
Build chatbots and conversational agents with message history:
|
|
161
103
|
|
|
162
104
|
```ruby
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
362
|
-
## Usage Guide
|
|
363
|
-
|
|
364
|
-
### Agent DSL
|
|
365
|
-
|
|
366
|
-
#### Model Configuration
|
|
367
|
-
|
|
368
|
-
```ruby
|
|
369
|
-
class MyAgent < ApplicationAgent
|
|
370
|
-
# LLM model to use
|
|
371
|
-
model "gpt-4o" # OpenAI GPT-4
|
|
372
|
-
# model "claude-3-5-sonnet" # Anthropic Claude
|
|
373
|
-
# model "gemini-2.0-flash" # Google Gemini (default)
|
|
374
|
-
|
|
375
|
-
# Randomness (0.0 = deterministic, 1.0 = creative)
|
|
376
|
-
temperature 0.7
|
|
377
|
-
|
|
378
|
-
# Version for cache key generation
|
|
379
|
-
version "2.0"
|
|
380
|
-
|
|
381
|
-
# Request timeout in seconds
|
|
382
|
-
timeout 30
|
|
383
|
-
|
|
384
|
-
# Enable caching with TTL
|
|
385
|
-
cache 1.hour
|
|
386
|
-
end
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
#### Parameter Definition
|
|
390
|
-
|
|
391
|
-
```ruby
|
|
392
|
-
class ProductSearchAgent < ApplicationAgent
|
|
393
|
-
# Required parameter - raises ArgumentError if not provided
|
|
394
|
-
param :query, required: true
|
|
395
|
-
|
|
396
|
-
# Optional parameter with default value
|
|
397
|
-
param :limit, default: 10
|
|
398
|
-
|
|
399
|
-
# Optional parameter (no default)
|
|
400
|
-
param :filters
|
|
401
|
-
|
|
402
|
-
# Multiple required parameters
|
|
403
|
-
param :user_id, required: true
|
|
404
|
-
param :session_id, required: true
|
|
405
|
-
end
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
#### Prompt Methods
|
|
409
|
-
|
|
410
|
-
```ruby
|
|
411
|
-
class ContentGeneratorAgent < ApplicationAgent
|
|
412
|
-
param :topic, required: true
|
|
413
|
-
param :tone, default: "professional"
|
|
414
|
-
param :word_count, default: 500
|
|
415
|
-
|
|
416
|
-
private
|
|
417
|
-
|
|
418
|
-
# System prompt (optional) - sets the AI's role and instructions
|
|
419
|
-
def system_prompt
|
|
420
|
-
<<~PROMPT
|
|
421
|
-
You are a professional content writer specializing in #{topic}.
|
|
422
|
-
Write in a #{tone} tone.
|
|
423
|
-
PROMPT
|
|
424
|
-
end
|
|
425
|
-
|
|
426
|
-
# User prompt (required) - the main request to the AI
|
|
427
|
-
def user_prompt
|
|
428
|
-
<<~PROMPT
|
|
429
|
-
Write a #{word_count}-word article about: #{topic}
|
|
430
|
-
|
|
431
|
-
Requirements:
|
|
432
|
-
- Clear structure with introduction, body, and conclusion
|
|
433
|
-
- Use examples and data where relevant
|
|
434
|
-
- Maintain a #{tone} tone throughout
|
|
435
|
-
PROMPT
|
|
436
|
-
end
|
|
437
|
-
end
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
#### Structured Output with Schema
|
|
441
|
-
|
|
442
|
-
```ruby
|
|
443
|
-
class EmailClassifierAgent < ApplicationAgent
|
|
444
|
-
param :email_content, required: true
|
|
445
|
-
|
|
446
|
-
private
|
|
447
|
-
|
|
448
|
-
def system_prompt
|
|
449
|
-
"You are an email classification system. Analyze emails and categorize them."
|
|
450
|
-
end
|
|
451
|
-
|
|
452
|
-
def user_prompt
|
|
453
|
-
email_content
|
|
454
|
-
end
|
|
455
|
-
|
|
456
|
-
def schema
|
|
457
|
-
@schema ||= RubyLLM::Schema.create do
|
|
458
|
-
string :category,
|
|
459
|
-
enum: ["urgent", "important", "spam", "newsletter", "personal"],
|
|
460
|
-
description: "Email category"
|
|
461
|
-
|
|
462
|
-
number :priority,
|
|
463
|
-
description: "Priority score from 0 to 10"
|
|
464
|
-
|
|
465
|
-
array :tags,
|
|
466
|
-
of: :string,
|
|
467
|
-
description: "Relevant tags for the email"
|
|
468
|
-
|
|
469
|
-
boolean :requires_response,
|
|
470
|
-
description: "Whether the email requires a response"
|
|
471
|
-
|
|
472
|
-
object :sender_info do
|
|
473
|
-
string :name, nullable: true
|
|
474
|
-
string :company, nullable: true
|
|
475
|
-
boolean :is_known_contact
|
|
476
|
-
end
|
|
477
|
-
end
|
|
478
|
-
end
|
|
479
|
-
end
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
#### Response Processing
|
|
483
|
-
|
|
484
|
-
```ruby
|
|
485
|
-
class DataExtractorAgent < ApplicationAgent
|
|
486
|
-
param :text, required: true
|
|
487
|
-
|
|
488
|
-
private
|
|
489
|
-
|
|
490
|
-
def user_prompt
|
|
491
|
-
"Extract key information from: #{text}"
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
def schema
|
|
495
|
-
@schema ||= RubyLLM::Schema.create do
|
|
496
|
-
string :summary
|
|
497
|
-
array :entities, of: :string
|
|
498
|
-
end
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
# Post-process the LLM response
|
|
502
|
-
def process_response(response)
|
|
503
|
-
result = super(response)
|
|
504
|
-
|
|
505
|
-
# Add custom processing
|
|
506
|
-
result[:entities] = result[:entities].map(&:downcase).uniq
|
|
507
|
-
result[:word_count] = result[:summary].split.length
|
|
508
|
-
result[:extracted_at] = Time.current
|
|
509
|
-
|
|
510
|
-
result
|
|
511
|
-
end
|
|
512
|
-
end
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
#### Custom Metadata
|
|
516
|
-
|
|
517
|
-
Add custom data to execution logs for filtering and analytics:
|
|
518
|
-
|
|
519
|
-
```ruby
|
|
520
|
-
class UserQueryAgent < ApplicationAgent
|
|
521
|
-
param :query, required: true
|
|
522
|
-
param :user_id, required: true
|
|
523
|
-
param :source, default: "web"
|
|
524
|
-
|
|
525
|
-
# This data will be stored in the execution record's metadata column
|
|
526
|
-
def execution_metadata
|
|
527
|
-
{
|
|
528
|
-
user_id: user_id,
|
|
529
|
-
source: source,
|
|
530
|
-
query_length: query.length,
|
|
531
|
-
timestamp: Time.current.iso8601
|
|
532
|
-
}
|
|
533
|
-
end
|
|
534
|
-
|
|
535
|
-
private
|
|
536
|
-
|
|
537
|
-
def user_prompt
|
|
538
|
-
query
|
|
539
|
-
end
|
|
540
|
-
end
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
### Advanced Examples
|
|
544
|
-
|
|
545
|
-
#### Multi-Step Agent with Conversation History
|
|
546
|
-
|
|
547
|
-
```ruby
|
|
548
|
-
class ConversationAgent < ApplicationAgent
|
|
549
|
-
param :messages, required: true # Array of {role:, content:} hashes
|
|
550
|
-
param :context, default: {}
|
|
551
|
-
|
|
552
|
-
def call
|
|
553
|
-
return dry_run_response if @options[:dry_run]
|
|
554
|
-
|
|
555
|
-
instrument_execution do
|
|
556
|
-
Timeout.timeout(self.class.timeout) do
|
|
557
|
-
client = build_client_with_messages(messages)
|
|
558
|
-
response = client.ask(user_prompt)
|
|
559
|
-
process_response(capture_response(response))
|
|
560
|
-
end
|
|
561
|
-
end
|
|
562
|
-
end
|
|
563
|
-
|
|
564
|
-
private
|
|
565
|
-
|
|
566
|
-
def system_prompt
|
|
567
|
-
"You are a helpful assistant. Remember the conversation context."
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
def user_prompt
|
|
571
|
-
messages.last[:content]
|
|
572
|
-
end
|
|
573
|
-
end
|
|
574
|
-
|
|
575
|
-
# Usage
|
|
576
|
-
ConversationAgent.call(
|
|
105
|
+
result = ChatAgent.call(
|
|
106
|
+
query: "What's my name?",
|
|
577
107
|
messages: [
|
|
578
|
-
{ role:
|
|
579
|
-
{ role:
|
|
580
|
-
{ role: "user", content: "Okay, tell me a joke then." }
|
|
108
|
+
{ role: :user, content: "My name is Alice" },
|
|
109
|
+
{ role: :assistant, content: "Nice to meet you, Alice!" }
|
|
581
110
|
]
|
|
582
111
|
)
|
|
112
|
+
# => "Your name is Alice!"
|
|
583
113
|
```
|
|
584
114
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
```ruby
|
|
588
|
-
class RecommendationAgent < ApplicationAgent
|
|
589
|
-
param :user_id, required: true
|
|
590
|
-
param :category, required: true
|
|
591
|
-
param :limit, default: 10
|
|
592
|
-
|
|
593
|
-
cache 30.minutes
|
|
594
|
-
|
|
595
|
-
private
|
|
115
|
+
See [Conversation History](https://github.com/adham90/ruby_llm-agents/wiki/Conversation-History) for more patterns.
|
|
596
116
|
|
|
597
|
-
|
|
598
|
-
# This excludes 'limit' from cache key, so different limits
|
|
599
|
-
# will return the same cached result
|
|
600
|
-
def cache_key_data
|
|
601
|
-
{ user_id: user_id, category: category }
|
|
602
|
-
end
|
|
117
|
+
## Documentation
|
|
603
118
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
119
|
+
| Guide | Description |
|
|
120
|
+
|-------|-------------|
|
|
121
|
+
| [Getting Started](https://github.com/adham90/ruby_llm-agents/wiki/Getting-Started) | Installation, configuration, first agent |
|
|
122
|
+
| [Agent DSL](https://github.com/adham90/ruby_llm-agents/wiki/Agent-DSL) | All DSL options: model, temperature, params, caching |
|
|
123
|
+
| [Reliability](https://github.com/adham90/ruby_llm-agents/wiki/Reliability) | Retries, fallbacks, circuit breakers, timeouts |
|
|
124
|
+
| [Workflows](https://github.com/adham90/ruby_llm-agents/wiki/Workflows) | Pipelines, parallel execution, routers |
|
|
125
|
+
| [Budget Controls](https://github.com/adham90/ruby_llm-agents/wiki/Budget-Controls) | Spending limits, alerts, enforcement |
|
|
126
|
+
| [Dashboard](https://github.com/adham90/ruby_llm-agents/wiki/Dashboard) | Setup, authentication, analytics |
|
|
127
|
+
| [Production](https://github.com/adham90/ruby_llm-agents/wiki/Production-Deployment) | Deployment best practices, background jobs |
|
|
128
|
+
| [API Reference](https://github.com/adham90/ruby_llm-agents/wiki/API-Reference) | Complete class documentation |
|
|
129
|
+
| [Examples](https://github.com/adham90/ruby_llm-agents/wiki/Examples) | Real-world use cases and patterns |
|
|
609
130
|
|
|
610
131
|
## Reliability Features
|
|
611
132
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
### Automatic Retries
|
|
615
|
-
|
|
616
|
-
Configure retry behavior for transient failures:
|
|
133
|
+
Build resilient agents with built-in fault tolerance:
|
|
617
134
|
|
|
618
135
|
```ruby
|
|
619
136
|
class ReliableAgent < ApplicationAgent
|
|
620
137
|
model "gpt-4o"
|
|
621
138
|
|
|
622
|
-
# Retry
|
|
623
|
-
retries max: 3, backoff: :exponential
|
|
624
|
-
|
|
625
|
-
# Only retry on specific errors (defaults include timeout, network errors)
|
|
626
|
-
retries max: 3, on: [Timeout::Error, Net::ReadTimeout, Faraday::TimeoutError]
|
|
627
|
-
|
|
628
|
-
param :query, required: true
|
|
629
|
-
|
|
630
|
-
def user_prompt
|
|
631
|
-
query
|
|
632
|
-
end
|
|
633
|
-
end
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
Backoff strategies:
|
|
637
|
-
- `:exponential` - Delay doubles each retry (0.5s, 1s, 2s, 4s...)
|
|
638
|
-
- `:constant` - Same delay each retry
|
|
639
|
-
- Jitter is automatically added to prevent thundering herd
|
|
640
|
-
|
|
641
|
-
### Model Fallbacks
|
|
642
|
-
|
|
643
|
-
Automatically try alternative models if the primary fails:
|
|
139
|
+
# Retry on failures with exponential backoff
|
|
140
|
+
retries max: 3, backoff: :exponential
|
|
644
141
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
model "gpt-4o"
|
|
142
|
+
# Fall back to alternative models
|
|
143
|
+
fallback_models "gpt-4o-mini", "claude-3-5-sonnet"
|
|
648
144
|
|
|
649
|
-
#
|
|
650
|
-
|
|
145
|
+
# Prevent cascading failures
|
|
146
|
+
circuit_breaker errors: 10, within: 60, cooldown: 300
|
|
651
147
|
|
|
652
|
-
#
|
|
653
|
-
|
|
654
|
-
fallback_models "gpt-4o-mini", "claude-3-sonnet"
|
|
148
|
+
# Maximum time for all attempts
|
|
149
|
+
total_timeout 30
|
|
655
150
|
|
|
656
151
|
param :query, required: true
|
|
657
152
|
|
|
@@ -661,739 +156,113 @@ class FallbackAgent < ApplicationAgent
|
|
|
661
156
|
end
|
|
662
157
|
```
|
|
663
158
|
|
|
664
|
-
|
|
159
|
+
## Workflow Orchestration
|
|
665
160
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
Prevent cascading failures by temporarily blocking requests to failing models:
|
|
161
|
+
Compose agents into complex workflows:
|
|
669
162
|
|
|
670
163
|
```ruby
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
# Open circuit after 10 errors within 60 seconds
|
|
676
|
-
# Keep circuit open for 5 minutes before retrying
|
|
677
|
-
circuit_breaker errors: 10, within: 60, cooldown: 300
|
|
678
|
-
|
|
679
|
-
param :query, required: true
|
|
164
|
+
# Sequential pipeline - each step's output feeds the next
|
|
165
|
+
class ContentPipeline < RubyLLM::Agents::Workflow::Pipeline
|
|
166
|
+
timeout 60.seconds
|
|
167
|
+
max_cost 1.00
|
|
680
168
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
169
|
+
step :classify, agent: ClassifierAgent
|
|
170
|
+
step :enrich, agent: EnricherAgent
|
|
171
|
+
step :format, agent: FormatterAgent, optional: true
|
|
684
172
|
end
|
|
685
|
-
```
|
|
686
|
-
|
|
687
|
-
Circuit breaker states:
|
|
688
|
-
- **Closed** - Normal operation, requests pass through
|
|
689
|
-
- **Open** - Model is blocked, requests skip to fallback or fail fast
|
|
690
|
-
- **Half-Open** - After cooldown, one request is allowed to test recovery
|
|
691
173
|
|
|
692
|
-
|
|
174
|
+
result = ContentPipeline.call(text: data)
|
|
175
|
+
result.steps[:classify].content # Individual step result
|
|
176
|
+
result.total_cost # Sum of all steps
|
|
693
177
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
class TimeBoundAgent < ApplicationAgent
|
|
698
|
-
model "gpt-4o"
|
|
699
|
-
retries max: 5
|
|
700
|
-
fallback_models "gpt-4o-mini"
|
|
701
|
-
|
|
702
|
-
# Abort everything after 30 seconds total
|
|
703
|
-
total_timeout 30
|
|
704
|
-
|
|
705
|
-
param :query, required: true
|
|
178
|
+
# Parallel execution - run agents concurrently
|
|
179
|
+
class AnalysisPipeline < RubyLLM::Agents::Workflow::Parallel
|
|
180
|
+
fail_fast false # Continue even if a branch fails
|
|
706
181
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
182
|
+
branch :sentiment, agent: SentimentAgent
|
|
183
|
+
branch :entities, agent: EntityAgent
|
|
184
|
+
branch :summary, agent: SummaryAgent
|
|
710
185
|
end
|
|
711
|
-
```
|
|
712
186
|
|
|
713
|
-
|
|
187
|
+
result = AnalysisPipeline.call(text: content)
|
|
188
|
+
result.branches[:sentiment].content # Individual branch result
|
|
714
189
|
|
|
715
|
-
|
|
190
|
+
# Conditional routing - dispatch based on classification
|
|
191
|
+
class SupportRouter < RubyLLM::Agents::Workflow::Router
|
|
192
|
+
classifier_model "gpt-4o-mini"
|
|
716
193
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
# Check if retries/fallbacks were used
|
|
721
|
-
execution.has_retries? # => true
|
|
722
|
-
execution.used_fallback? # => true
|
|
723
|
-
execution.attempts_count # => 3
|
|
724
|
-
|
|
725
|
-
# Get attempt details
|
|
726
|
-
execution.attempts.each do |attempt|
|
|
727
|
-
puts "Model: #{attempt['model_id']}"
|
|
728
|
-
puts "Duration: #{attempt['duration_ms']}ms"
|
|
729
|
-
puts "Error: #{attempt['error_class']}" if attempt['error_class']
|
|
730
|
-
puts "Short-circuited: #{attempt['short_circuited']}"
|
|
194
|
+
route :support, to: SupportAgent, description: "Technical support issues"
|
|
195
|
+
route :sales, to: SalesAgent, description: "Sales and pricing questions"
|
|
196
|
+
route :default, to: GeneralAgent
|
|
731
197
|
end
|
|
732
198
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
execution.chosen_model_id # => "claude-3-sonnet" (the model that succeeded)
|
|
199
|
+
result = SupportRouter.call(message: user_input)
|
|
200
|
+
result.routed_to # :support, :sales, or :default
|
|
736
201
|
```
|
|
737
202
|
|
|
738
|
-
##
|
|
739
|
-
|
|
740
|
-
### Budget Limits
|
|
203
|
+
## Cost & Budget Controls
|
|
741
204
|
|
|
742
|
-
|
|
205
|
+
Track and limit LLM spending:
|
|
743
206
|
|
|
744
207
|
```ruby
|
|
745
208
|
# config/initializers/ruby_llm_agents.rb
|
|
746
209
|
RubyLLM::Agents.configure do |config|
|
|
747
210
|
config.budgets = {
|
|
748
|
-
#
|
|
749
|
-
|
|
750
|
-
global_monthly: 2000.0, # $2000/month across all agents
|
|
751
|
-
|
|
752
|
-
# Per-agent limits
|
|
211
|
+
global_daily: 100.0, # $100/day limit
|
|
212
|
+
global_monthly: 2000.0, # $2000/month limit
|
|
753
213
|
per_agent_daily: {
|
|
754
|
-
"ExpensiveAgent" => 50.0
|
|
755
|
-
"CheapAgent" => 5.0 # $5/day for this agent
|
|
214
|
+
"ExpensiveAgent" => 50.0
|
|
756
215
|
},
|
|
757
|
-
|
|
758
|
-
"ExpensiveAgent" => 500.0
|
|
759
|
-
},
|
|
760
|
-
|
|
761
|
-
# Enforcement mode
|
|
762
|
-
# :hard - Block requests when budget exceeded
|
|
763
|
-
# :soft - Allow requests but log warnings
|
|
764
|
-
enforcement: :hard
|
|
216
|
+
enforcement: :hard # Block when exceeded
|
|
765
217
|
}
|
|
766
|
-
end
|
|
767
|
-
```
|
|
768
218
|
|
|
769
|
-
Querying budget status:
|
|
770
|
-
|
|
771
|
-
```ruby
|
|
772
|
-
# Get current budget status
|
|
773
|
-
status = RubyLLM::Agents::BudgetTracker.status(agent_type: "MyAgent")
|
|
774
|
-
# => {
|
|
775
|
-
# global_daily: { limit: 100.0, current: 45.50, remaining: 54.50, percentage_used: 45.5 },
|
|
776
|
-
# global_monthly: { limit: 2000.0, current: 890.0, remaining: 1110.0, percentage_used: 44.5 }
|
|
777
|
-
# }
|
|
778
|
-
|
|
779
|
-
# Check remaining budget
|
|
780
|
-
RubyLLM::Agents::BudgetTracker.remaining_budget(:global, :daily)
|
|
781
|
-
# => 54.50
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
### Alerts
|
|
785
|
-
|
|
786
|
-
Get notified when important events occur:
|
|
787
|
-
|
|
788
|
-
```ruby
|
|
789
|
-
# config/initializers/ruby_llm_agents.rb
|
|
790
|
-
RubyLLM::Agents.configure do |config|
|
|
791
219
|
config.alerts = {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
:budget_soft_cap, # Budget threshold reached (configurable %)
|
|
795
|
-
:budget_hard_cap, # Budget exceeded (with hard enforcement)
|
|
796
|
-
:breaker_open # Circuit breaker opened
|
|
797
|
-
],
|
|
798
|
-
|
|
799
|
-
# Slack webhook
|
|
800
|
-
slack_webhook_url: ENV['SLACK_WEBHOOK_URL'],
|
|
801
|
-
|
|
802
|
-
# Generic webhook (receives JSON payload)
|
|
803
|
-
webhook_url: "https://your-app.com/webhooks/llm-alerts",
|
|
804
|
-
|
|
805
|
-
# Custom handler
|
|
806
|
-
custom: ->(event, payload) {
|
|
807
|
-
# event: :budget_hard_cap
|
|
808
|
-
# payload: { scope: :global_daily, limit: 100.0, current: 105.0 }
|
|
809
|
-
|
|
810
|
-
MyNotificationService.notify(
|
|
811
|
-
title: "LLM Budget Alert",
|
|
812
|
-
message: "#{event}: #{payload}"
|
|
813
|
-
)
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
end
|
|
817
|
-
```
|
|
818
|
-
|
|
819
|
-
Alert payload examples:
|
|
820
|
-
|
|
821
|
-
```ruby
|
|
822
|
-
# Budget alert
|
|
823
|
-
{
|
|
824
|
-
event: :budget_hard_cap,
|
|
825
|
-
scope: :global_daily,
|
|
826
|
-
limit: 100.0,
|
|
827
|
-
current: 105.50,
|
|
828
|
-
agent_type: "ExpensiveAgent"
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
# Circuit breaker alert
|
|
832
|
-
{
|
|
833
|
-
event: :breaker_open,
|
|
834
|
-
agent_type: "MyAgent",
|
|
835
|
-
model_id: "gpt-4o",
|
|
836
|
-
failure_count: 10,
|
|
837
|
-
window_seconds: 60
|
|
838
|
-
}
|
|
839
|
-
```
|
|
840
|
-
|
|
841
|
-
### PII Redaction
|
|
842
|
-
|
|
843
|
-
Automatically redact sensitive data from execution logs:
|
|
844
|
-
|
|
845
|
-
```ruby
|
|
846
|
-
# config/initializers/ruby_llm_agents.rb
|
|
847
|
-
RubyLLM::Agents.configure do |config|
|
|
848
|
-
config.redaction = {
|
|
849
|
-
# Fields to redact (applied to parameters)
|
|
850
|
-
# Default: password, token, api_key, secret, credential, auth, key, access_token
|
|
851
|
-
fields: %w[ssn credit_card phone_number],
|
|
852
|
-
|
|
853
|
-
# Regex patterns to redact from prompts/responses
|
|
854
|
-
patterns: [
|
|
855
|
-
/\b\d{3}-\d{2}-\d{4}\b/, # SSN
|
|
856
|
-
/\b\d{16}\b/, # Credit card
|
|
857
|
-
/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i # Email
|
|
858
|
-
],
|
|
859
|
-
|
|
860
|
-
# Replacement text
|
|
861
|
-
placeholder: "[REDACTED]",
|
|
862
|
-
|
|
863
|
-
# Truncate long values
|
|
864
|
-
max_value_length: 1000
|
|
220
|
+
on_events: [:budget_soft_cap, :budget_hard_cap, :breaker_open],
|
|
221
|
+
slack_webhook_url: ENV['SLACK_WEBHOOK_URL']
|
|
865
222
|
}
|
|
866
|
-
|
|
867
|
-
# Control what gets persisted
|
|
868
|
-
config.persist_prompts = true # Store system/user prompts
|
|
869
|
-
config.persist_responses = true # Store LLM responses
|
|
870
|
-
end
|
|
871
|
-
```
|
|
872
|
-
|
|
873
|
-
## Configuration
|
|
874
|
-
|
|
875
|
-
Edit `config/initializers/ruby_llm_agents.rb`:
|
|
876
|
-
|
|
877
|
-
```ruby
|
|
878
|
-
RubyLLM::Agents.configure do |config|
|
|
879
|
-
# ============================================================================
|
|
880
|
-
# Default Settings for All Agents
|
|
881
|
-
# ============================================================================
|
|
882
|
-
|
|
883
|
-
# Default LLM model (can be overridden per agent)
|
|
884
|
-
config.default_model = "gemini-2.0-flash"
|
|
885
|
-
|
|
886
|
-
# Default temperature (0.0 = deterministic, 1.0 = creative)
|
|
887
|
-
config.default_temperature = 0.0
|
|
888
|
-
|
|
889
|
-
# Default timeout for LLM requests (in seconds)
|
|
890
|
-
config.default_timeout = 60
|
|
891
|
-
|
|
892
|
-
# Enable streaming by default for all agents
|
|
893
|
-
config.default_streaming = false
|
|
894
|
-
|
|
895
|
-
# ============================================================================
|
|
896
|
-
# Caching Configuration
|
|
897
|
-
# ============================================================================
|
|
898
|
-
|
|
899
|
-
# Cache store for agent responses (default: Rails.cache)
|
|
900
|
-
config.cache_store = Rails.cache
|
|
901
|
-
# config.cache_store = ActiveSupport::Cache::MemoryStore.new
|
|
902
|
-
# config.cache_store = ActiveSupport::Cache::RedisCacheStore.new(url: ENV['REDIS_URL'])
|
|
903
|
-
|
|
904
|
-
# ============================================================================
|
|
905
|
-
# Execution Logging
|
|
906
|
-
# ============================================================================
|
|
907
|
-
|
|
908
|
-
# Use background job for logging (recommended for production)
|
|
909
|
-
config.async_logging = true
|
|
910
|
-
|
|
911
|
-
# How long to retain execution records (for cleanup tasks)
|
|
912
|
-
config.retention_period = 30.days
|
|
913
|
-
|
|
914
|
-
# ============================================================================
|
|
915
|
-
# Anomaly Detection
|
|
916
|
-
# ============================================================================
|
|
917
|
-
|
|
918
|
-
# Log warning if an execution costs more than this (in dollars)
|
|
919
|
-
config.anomaly_cost_threshold = 5.00
|
|
920
|
-
|
|
921
|
-
# Log warning if an execution takes longer than this (in milliseconds)
|
|
922
|
-
config.anomaly_duration_threshold = 10_000 # 10 seconds
|
|
923
|
-
|
|
924
|
-
# ============================================================================
|
|
925
|
-
# Dashboard Configuration
|
|
926
|
-
# ============================================================================
|
|
927
|
-
|
|
928
|
-
# Authentication for dashboard access
|
|
929
|
-
# Return true to allow access, false to deny
|
|
930
|
-
config.dashboard_auth = ->(controller) {
|
|
931
|
-
controller.current_user&.admin?
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
# Customize the parent controller for dashboard
|
|
935
|
-
config.dashboard_parent_controller = "ApplicationController"
|
|
936
223
|
end
|
|
937
224
|
```
|
|
938
225
|
|
|
939
226
|
## Dashboard
|
|
940
227
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
The install generator automatically mounts the dashboard, but you can customize the path:
|
|
228
|
+
Mount the real-time monitoring dashboard:
|
|
944
229
|
|
|
945
230
|
```ruby
|
|
946
231
|
# config/routes.rb
|
|
947
232
|
mount RubyLLM::Agents::Engine => "/agents"
|
|
948
|
-
# or
|
|
949
|
-
mount RubyLLM::Agents::Engine => "/admin/ai-agents", as: "agents_dashboard"
|
|
950
|
-
```
|
|
951
|
-
|
|
952
|
-
### Dashboard Features
|
|
953
|
-
|
|
954
|
-
The dashboard provides:
|
|
955
|
-
|
|
956
|
-
1. **Overview Page** (`/agents`)
|
|
957
|
-
- Today's execution stats (total, success rate, failures)
|
|
958
|
-
- Real-time cost tracking
|
|
959
|
-
- Performance trends (7-day chart)
|
|
960
|
-
- Top agents by usage
|
|
961
|
-
|
|
962
|
-
2. **Executions List** (`/agents/executions`)
|
|
963
|
-
- Filterable by agent type, status, date range
|
|
964
|
-
- Sortable by cost, duration, timestamp
|
|
965
|
-
- Real-time updates via Turbo Streams
|
|
966
|
-
- Search by parameters
|
|
967
|
-
|
|
968
|
-
3. **Execution Detail** (`/agents/executions/:id`)
|
|
969
|
-
- Full system and user prompts
|
|
970
|
-
- Complete LLM response
|
|
971
|
-
- Token usage breakdown (input, output, cached)
|
|
972
|
-
- Cost calculation
|
|
973
|
-
- Execution metadata
|
|
974
|
-
- Error details (if failed)
|
|
975
|
-
|
|
976
|
-
### Authentication
|
|
977
|
-
|
|
978
|
-
Protect your dashboard by configuring authentication:
|
|
979
|
-
|
|
980
|
-
```ruby
|
|
981
|
-
# config/initializers/ruby_llm_agents.rb
|
|
982
|
-
RubyLLM::Agents.configure do |config|
|
|
983
|
-
config.dashboard_auth = ->(controller) {
|
|
984
|
-
# Example: Devise authentication
|
|
985
|
-
controller.authenticate_user! && controller.current_user.admin?
|
|
986
|
-
|
|
987
|
-
# Example: Basic auth
|
|
988
|
-
# controller.authenticate_or_request_with_http_basic do |username, password|
|
|
989
|
-
# username == ENV['DASHBOARD_USERNAME'] &&
|
|
990
|
-
# password == ENV['DASHBOARD_PASSWORD']
|
|
991
|
-
# end
|
|
992
|
-
|
|
993
|
-
# Example: IP whitelist
|
|
994
|
-
# ['127.0.0.1', '::1'].include?(controller.request.remote_ip)
|
|
995
|
-
}
|
|
996
|
-
end
|
|
997
|
-
```
|
|
998
|
-
|
|
999
|
-
## Analytics & Reporting
|
|
1000
|
-
|
|
1001
|
-
Query execution data programmatically:
|
|
1002
|
-
|
|
1003
|
-
### Daily Reports
|
|
1004
|
-
|
|
1005
|
-
```ruby
|
|
1006
|
-
# Get today's summary
|
|
1007
|
-
report = RubyLLM::Agents::Execution.daily_report
|
|
1008
|
-
# => {
|
|
1009
|
-
# total_executions: 1250,
|
|
1010
|
-
# successful: 1180,
|
|
1011
|
-
# failed: 70,
|
|
1012
|
-
# success_rate: 94.4,
|
|
1013
|
-
# total_cost: 12.45,
|
|
1014
|
-
# avg_duration_ms: 850,
|
|
1015
|
-
# total_tokens: 450000
|
|
1016
|
-
# }
|
|
1017
|
-
```
|
|
1018
|
-
|
|
1019
|
-
### Cost Analysis
|
|
1020
|
-
|
|
1021
|
-
```ruby
|
|
1022
|
-
# Cost breakdown by agent for this week
|
|
1023
|
-
costs = RubyLLM::Agents::Execution.cost_by_agent(period: :this_week)
|
|
1024
|
-
# => [
|
|
1025
|
-
# { agent_type: "SearchIntentAgent", total_cost: 5.67, executions: 450 },
|
|
1026
|
-
# { agent_type: "ContentGeneratorAgent", total_cost: 3.21, executions: 120 }
|
|
1027
|
-
# ]
|
|
1028
|
-
|
|
1029
|
-
# Cost breakdown by model
|
|
1030
|
-
costs = RubyLLM::Agents::Execution.cost_by_model(period: :today)
|
|
1031
|
-
```
|
|
1032
|
-
|
|
1033
|
-
### Agent Statistics
|
|
1034
|
-
|
|
1035
|
-
```ruby
|
|
1036
|
-
# Stats for a specific agent
|
|
1037
|
-
stats = RubyLLM::Agents::Execution.stats_for("SearchIntentAgent", period: :today)
|
|
1038
|
-
# => {
|
|
1039
|
-
# total: 150,
|
|
1040
|
-
# successful: 145,
|
|
1041
|
-
# failed: 5,
|
|
1042
|
-
# success_rate: 96.67,
|
|
1043
|
-
# avg_cost: 0.012,
|
|
1044
|
-
# total_cost: 1.80,
|
|
1045
|
-
# avg_duration_ms: 450,
|
|
1046
|
-
# total_tokens: 75000
|
|
1047
|
-
# }
|
|
1048
|
-
```
|
|
1049
|
-
|
|
1050
|
-
### Version Comparison
|
|
1051
|
-
|
|
1052
|
-
```ruby
|
|
1053
|
-
# Compare two versions of an agent
|
|
1054
|
-
comparison = RubyLLM::Agents::Execution.compare_versions(
|
|
1055
|
-
"SearchIntentAgent",
|
|
1056
|
-
"1.0",
|
|
1057
|
-
"2.0",
|
|
1058
|
-
period: :this_week
|
|
1059
|
-
)
|
|
1060
|
-
# => {
|
|
1061
|
-
# "1.0" => { total: 450, success_rate: 94.2, avg_cost: 0.015 },
|
|
1062
|
-
# "2.0" => { total: 550, success_rate: 96.8, avg_cost: 0.012 }
|
|
1063
|
-
# }
|
|
1064
233
|
```
|
|
1065
234
|
|
|
1066
|
-
|
|
235
|
+

|
|
1067
236
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
# => [
|
|
1075
|
-
# { date: "2024-01-01", executions: 120, cost: 1.45, avg_duration: 450 },
|
|
1076
|
-
# { date: "2024-01-02", executions: 135, cost: 1.62, avg_duration: 430 },
|
|
1077
|
-
# ...
|
|
1078
|
-
# ]
|
|
1079
|
-
```
|
|
1080
|
-
|
|
1081
|
-
### Streaming Analytics
|
|
237
|
+
Features:
|
|
238
|
+
- Execution history with filtering and search
|
|
239
|
+
- Cost analytics by agent, model, and time period
|
|
240
|
+
- Performance trends and charts
|
|
241
|
+
- Token usage breakdowns
|
|
242
|
+
- Error tracking and debugging
|
|
1082
243
|
|
|
1083
|
-
|
|
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
|
-
|
|
1093
|
-
### Scopes
|
|
1094
|
-
|
|
1095
|
-
Chain scopes for complex queries:
|
|
1096
|
-
|
|
1097
|
-
```ruby
|
|
1098
|
-
# All successful executions today
|
|
1099
|
-
RubyLLM::Agents::Execution.today.successful
|
|
1100
|
-
|
|
1101
|
-
# Failed executions for specific agent
|
|
1102
|
-
RubyLLM::Agents::Execution.by_agent("SearchIntentAgent").failed
|
|
1103
|
-
|
|
1104
|
-
# Expensive executions this week
|
|
1105
|
-
RubyLLM::Agents::Execution.this_week.expensive(1.00) # cost > $1
|
|
1106
|
-
|
|
1107
|
-
# Slow executions
|
|
1108
|
-
RubyLLM::Agents::Execution.slow(5000) # duration > 5 seconds
|
|
1109
|
-
|
|
1110
|
-
# Complex query
|
|
1111
|
-
expensive_slow_failures = RubyLLM::Agents::Execution
|
|
1112
|
-
.this_week
|
|
1113
|
-
.by_agent("ContentGeneratorAgent")
|
|
1114
|
-
.failed
|
|
1115
|
-
.expensive(0.50)
|
|
1116
|
-
.slow(3000)
|
|
1117
|
-
.order(created_at: :desc)
|
|
1118
|
-
```
|
|
1119
|
-
|
|
1120
|
-
### Available Scopes
|
|
1121
|
-
|
|
1122
|
-
```ruby
|
|
1123
|
-
# Time-based
|
|
1124
|
-
.today
|
|
1125
|
-
.this_week
|
|
1126
|
-
.this_month
|
|
1127
|
-
.yesterday
|
|
1128
|
-
|
|
1129
|
-
# Status
|
|
1130
|
-
.successful
|
|
1131
|
-
.failed
|
|
1132
|
-
.status_error
|
|
1133
|
-
.status_timeout
|
|
1134
|
-
.status_running
|
|
1135
|
-
|
|
1136
|
-
# Agent/Model
|
|
1137
|
-
.by_agent("AgentName")
|
|
1138
|
-
.by_model("gpt-4o")
|
|
1139
|
-
|
|
1140
|
-
# Performance
|
|
1141
|
-
.expensive(threshold) # cost > threshold
|
|
1142
|
-
.slow(milliseconds) # duration > ms
|
|
1143
|
-
|
|
1144
|
-
# Token usage
|
|
1145
|
-
.high_token_usage(threshold)
|
|
1146
|
-
|
|
1147
|
-
# Streaming
|
|
1148
|
-
.streaming
|
|
1149
|
-
.non_streaming
|
|
1150
|
-
```
|
|
1151
|
-
|
|
1152
|
-
## Generators
|
|
1153
|
-
|
|
1154
|
-
### Agent Generator
|
|
1155
|
-
|
|
1156
|
-
```bash
|
|
1157
|
-
# Basic agent
|
|
1158
|
-
rails generate ruby_llm_agents:agent MyAgent
|
|
1159
|
-
|
|
1160
|
-
# Agent with parameters
|
|
1161
|
-
rails generate ruby_llm_agents:agent SearchAgent query:required limit:10 filters
|
|
1162
|
-
|
|
1163
|
-
# Agent with custom model and temperature
|
|
1164
|
-
rails generate ruby_llm_agents:agent ContentAgent \
|
|
1165
|
-
topic:required \
|
|
1166
|
-
--model=gpt-4o \
|
|
1167
|
-
--temperature=0.7
|
|
1168
|
-
|
|
1169
|
-
# Agent with caching
|
|
1170
|
-
rails generate ruby_llm_agents:agent CachedAgent \
|
|
1171
|
-
key:required \
|
|
1172
|
-
--cache=1.hour
|
|
1173
|
-
```
|
|
1174
|
-
|
|
1175
|
-
### Install Generator
|
|
1176
|
-
|
|
1177
|
-
```bash
|
|
1178
|
-
# Initial setup
|
|
1179
|
-
rails generate ruby_llm_agents:install
|
|
1180
|
-
```
|
|
1181
|
-
|
|
1182
|
-
### Upgrade Generator
|
|
1183
|
-
|
|
1184
|
-
```bash
|
|
1185
|
-
# Upgrade to latest schema (when gem is updated)
|
|
1186
|
-
rails generate ruby_llm_agents:upgrade
|
|
1187
|
-
rails db:migrate
|
|
1188
|
-
```
|
|
1189
|
-
|
|
1190
|
-
This creates migrations for new features like:
|
|
1191
|
-
- `system_prompt` and `user_prompt` columns for prompt persistence
|
|
1192
|
-
- `attempts` JSONB column for reliability tracking
|
|
1193
|
-
- `chosen_model_id` for fallback model tracking
|
|
1194
|
-
|
|
1195
|
-
## Background Jobs
|
|
1196
|
-
|
|
1197
|
-
For production environments, enable async logging:
|
|
1198
|
-
|
|
1199
|
-
```ruby
|
|
1200
|
-
# config/initializers/ruby_llm_agents.rb
|
|
1201
|
-
RubyLLM::Agents.configure do |config|
|
|
1202
|
-
config.async_logging = true
|
|
1203
|
-
end
|
|
1204
|
-
```
|
|
1205
|
-
|
|
1206
|
-
This uses `RubyLLM::Agents::ExecutionLoggerJob` to log executions in the background.
|
|
1207
|
-
|
|
1208
|
-
Make sure you have a job processor running:
|
|
1209
|
-
|
|
1210
|
-
```bash
|
|
1211
|
-
# Using Solid Queue (Rails 7.1+)
|
|
1212
|
-
bin/jobs
|
|
1213
|
-
|
|
1214
|
-
# Or Sidekiq
|
|
1215
|
-
bundle exec sidekiq
|
|
1216
|
-
```
|
|
1217
|
-
|
|
1218
|
-
## Maintenance Tasks
|
|
1219
|
-
|
|
1220
|
-
### Cleanup Old Executions
|
|
1221
|
-
|
|
1222
|
-
```ruby
|
|
1223
|
-
# In a rake task or scheduled job
|
|
1224
|
-
retention_period = RubyLLM::Agents.configuration.retention_period
|
|
1225
|
-
RubyLLM::Agents::Execution.where("created_at < ?", retention_period.ago).delete_all
|
|
1226
|
-
```
|
|
1227
|
-
|
|
1228
|
-
### Export Data
|
|
1229
|
-
|
|
1230
|
-
```ruby
|
|
1231
|
-
# Export to CSV
|
|
1232
|
-
require 'csv'
|
|
1233
|
-
|
|
1234
|
-
CSV.open("agent_executions.csv", "wb") do |csv|
|
|
1235
|
-
csv << ["Agent", "Status", "Cost", "Duration", "Timestamp"]
|
|
1236
|
-
|
|
1237
|
-
RubyLLM::Agents::Execution.this_month.find_each do |execution|
|
|
1238
|
-
csv << [
|
|
1239
|
-
execution.agent_type,
|
|
1240
|
-
execution.status,
|
|
1241
|
-
execution.total_cost,
|
|
1242
|
-
execution.duration_ms,
|
|
1243
|
-
execution.created_at
|
|
1244
|
-
]
|
|
1245
|
-
end
|
|
1246
|
-
end
|
|
1247
|
-
```
|
|
1248
|
-
|
|
1249
|
-
## Testing
|
|
1250
|
-
|
|
1251
|
-
### RSpec Example
|
|
1252
|
-
|
|
1253
|
-
```ruby
|
|
1254
|
-
# spec/agents/search_intent_agent_spec.rb
|
|
1255
|
-
require 'rails_helper'
|
|
1256
|
-
|
|
1257
|
-
RSpec.describe SearchIntentAgent do
|
|
1258
|
-
describe ".call" do
|
|
1259
|
-
it "extracts search intent from query" do
|
|
1260
|
-
result = described_class.call(
|
|
1261
|
-
query: "red summer dress under $50",
|
|
1262
|
-
dry_run: true # Use dry_run for testing without API calls
|
|
1263
|
-
)
|
|
1264
|
-
|
|
1265
|
-
expect(result[:dry_run]).to be true
|
|
1266
|
-
expect(result[:agent]).to eq("SearchIntentAgent")
|
|
1267
|
-
end
|
|
1268
|
-
end
|
|
1269
|
-
|
|
1270
|
-
describe "parameter validation" do
|
|
1271
|
-
it "requires query parameter" do
|
|
1272
|
-
expect {
|
|
1273
|
-
described_class.call(limit: 10)
|
|
1274
|
-
}.to raise_error(ArgumentError, /missing required params/)
|
|
1275
|
-
end
|
|
1276
|
-
|
|
1277
|
-
it "uses default limit" do
|
|
1278
|
-
agent = described_class.new(query: "test")
|
|
1279
|
-
expect(agent.limit).to eq(10)
|
|
1280
|
-
end
|
|
1281
|
-
end
|
|
1282
|
-
end
|
|
1283
|
-
```
|
|
1284
|
-
|
|
1285
|
-
### Mocking LLM Responses
|
|
1286
|
-
|
|
1287
|
-
```ruby
|
|
1288
|
-
# spec/support/llm_helpers.rb
|
|
1289
|
-
module LLMHelpers
|
|
1290
|
-
def mock_llm_response(data)
|
|
1291
|
-
response = instance_double(RubyLLM::Response, content: data)
|
|
1292
|
-
allow_any_instance_of(RubyLLM::Chat).to receive(:ask).and_return(response)
|
|
1293
|
-
end
|
|
1294
|
-
end
|
|
1295
|
-
|
|
1296
|
-
# In your spec
|
|
1297
|
-
RSpec.describe SearchIntentAgent do
|
|
1298
|
-
include LLMHelpers
|
|
1299
|
-
|
|
1300
|
-
it "processes search intent" do
|
|
1301
|
-
mock_llm_response({
|
|
1302
|
-
refined_query: "red dress",
|
|
1303
|
-
filters: ["color:red"],
|
|
1304
|
-
category_id: 42
|
|
1305
|
-
})
|
|
1306
|
-
|
|
1307
|
-
result = described_class.call(query: "red summer dress")
|
|
1308
|
-
|
|
1309
|
-
expect(result[:refined_query]).to eq("red dress")
|
|
1310
|
-
expect(result[:filters]).to include("color:red")
|
|
1311
|
-
end
|
|
1312
|
-
end
|
|
1313
|
-
```
|
|
1314
|
-
|
|
1315
|
-
## Development
|
|
1316
|
-
|
|
1317
|
-
After checking out the repo:
|
|
1318
|
-
|
|
1319
|
-
```bash
|
|
1320
|
-
# Install dependencies
|
|
1321
|
-
bin/setup
|
|
1322
|
-
|
|
1323
|
-
# Run tests
|
|
1324
|
-
bundle exec rake spec
|
|
1325
|
-
|
|
1326
|
-
# Run linter
|
|
1327
|
-
bundle exec standardrb
|
|
1328
|
-
|
|
1329
|
-
# Fix linting issues
|
|
1330
|
-
bundle exec standardrb --fix
|
|
1331
|
-
|
|
1332
|
-
# Run console
|
|
1333
|
-
bin/rails console
|
|
1334
|
-
```
|
|
1335
|
-
|
|
1336
|
-
## Troubleshooting
|
|
1337
|
-
|
|
1338
|
-
### Agent execution fails with timeout
|
|
1339
|
-
|
|
1340
|
-
Increase timeout for specific agent:
|
|
1341
|
-
|
|
1342
|
-
```ruby
|
|
1343
|
-
class SlowAgent < ApplicationAgent
|
|
1344
|
-
timeout 120 # 2 minutes
|
|
1345
|
-
end
|
|
1346
|
-
```
|
|
1347
|
-
|
|
1348
|
-
### Cache not working
|
|
1349
|
-
|
|
1350
|
-
Ensure Rails cache is configured:
|
|
1351
|
-
|
|
1352
|
-
```ruby
|
|
1353
|
-
# config/environments/production.rb
|
|
1354
|
-
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
|
|
1355
|
-
```
|
|
1356
|
-
|
|
1357
|
-
### Dashboard not accessible
|
|
1358
|
-
|
|
1359
|
-
Check route mounting and authentication:
|
|
1360
|
-
|
|
1361
|
-
```ruby
|
|
1362
|
-
# config/routes.rb
|
|
1363
|
-
mount RubyLLM::Agents::Engine => "/agents"
|
|
1364
|
-
|
|
1365
|
-
# config/initializers/ruby_llm_agents.rb
|
|
1366
|
-
config.dashboard_auth = ->(controller) { true } # Allow all (dev only!)
|
|
1367
|
-
```
|
|
1368
|
-
|
|
1369
|
-
### High costs
|
|
1370
|
-
|
|
1371
|
-
Monitor and set limits:
|
|
1372
|
-
|
|
1373
|
-
```ruby
|
|
1374
|
-
# config/initializers/ruby_llm_agents.rb
|
|
1375
|
-
config.anomaly_cost_threshold = 1.00 # Alert at $1
|
|
244
|
+
## Requirements
|
|
1376
245
|
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
246
|
+
- **Ruby** >= 3.1.0
|
|
247
|
+
- **Rails** >= 7.0
|
|
248
|
+
- **RubyLLM** >= 1.0
|
|
1380
249
|
|
|
1381
250
|
## Contributing
|
|
1382
251
|
|
|
1383
|
-
Bug reports and pull requests are welcome
|
|
252
|
+
Bug reports and pull requests are welcome at [GitHub](https://github.com/adham90/ruby_llm-agents).
|
|
1384
253
|
|
|
1385
254
|
1. Fork the repository
|
|
1386
|
-
2. Create your feature branch (`git checkout -b my-
|
|
1387
|
-
3. Commit your changes (`git commit -am 'Add
|
|
1388
|
-
4. Push to the branch (`git push origin my-
|
|
1389
|
-
5. Create a
|
|
255
|
+
2. Create your feature branch (`git checkout -b my-feature`)
|
|
256
|
+
3. Commit your changes (`git commit -am 'Add feature'`)
|
|
257
|
+
4. Push to the branch (`git push origin my-feature`)
|
|
258
|
+
5. Create a Pull Request
|
|
1390
259
|
|
|
1391
260
|
## License
|
|
1392
261
|
|
|
1393
|
-
The gem is available as open source under the
|
|
262
|
+
The gem is available as open source under the [MIT License](https://opensource.org/licenses/MIT).
|
|
1394
263
|
|
|
1395
264
|
## Credits
|
|
1396
265
|
|
|
1397
|
-
Built with
|
|
266
|
+
Built with love by [Adham Eldeeb](https://github.com/adham90)
|
|
1398
267
|
|
|
1399
268
|
Powered by [RubyLLM](https://github.com/crmne/ruby_llm)
|