ruby_llm-agents 0.1.0
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +898 -0
- data/app/channels/ruby_llm/agents/executions_channel.rb +23 -0
- data/app/controllers/ruby_llm/agents/agents_controller.rb +100 -0
- data/app/controllers/ruby_llm/agents/application_controller.rb +20 -0
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +34 -0
- data/app/controllers/ruby_llm/agents/executions_controller.rb +93 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +149 -0
- data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +56 -0
- data/app/javascript/ruby_llm/agents/controllers/index.js +12 -0
- data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +83 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +166 -0
- data/app/models/ruby_llm/agents/execution/metrics.rb +89 -0
- data/app/models/ruby_llm/agents/execution/scopes.rb +81 -0
- data/app/models/ruby_llm/agents/execution.rb +81 -0
- data/app/services/ruby_llm/agents/agent_registry.rb +112 -0
- data/app/views/layouts/rubyllm/agents/application.html.erb +276 -0
- data/app/views/rubyllm/agents/agents/index.html.erb +89 -0
- data/app/views/rubyllm/agents/agents/show.html.erb +562 -0
- data/app/views/rubyllm/agents/dashboard/_execution_item.html.erb +48 -0
- data/app/views/rubyllm/agents/dashboard/index.html.erb +121 -0
- data/app/views/rubyllm/agents/executions/_execution.html.erb +64 -0
- data/app/views/rubyllm/agents/executions/_filters.html.erb +172 -0
- data/app/views/rubyllm/agents/executions/_list.html.erb +229 -0
- data/app/views/rubyllm/agents/executions/index.html.erb +83 -0
- data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +4 -0
- data/app/views/rubyllm/agents/executions/show.html.erb +240 -0
- data/app/views/rubyllm/agents/shared/_executions_table.html.erb +193 -0
- data/app/views/rubyllm/agents/shared/_stat_card.html.erb +14 -0
- data/app/views/rubyllm/agents/shared/_status_badge.html.erb +65 -0
- data/app/views/rubyllm/agents/shared/_status_dot.html.erb +18 -0
- data/config/routes.rb +13 -0
- data/lib/generators/ruby_llm_agents/agent_generator.rb +79 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +89 -0
- data/lib/generators/ruby_llm_agents/templates/add_prompts_migration.rb.tt +12 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +46 -0
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +22 -0
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +36 -0
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +66 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +59 -0
- data/lib/ruby_llm/agents/base.rb +271 -0
- data/lib/ruby_llm/agents/configuration.rb +36 -0
- data/lib/ruby_llm/agents/engine.rb +32 -0
- data/lib/ruby_llm/agents/execution_logger_job.rb +59 -0
- data/lib/ruby_llm/agents/inflections.rb +13 -0
- data/lib/ruby_llm/agents/instrumentation.rb +245 -0
- data/lib/ruby_llm/agents/version.rb +7 -0
- data/lib/ruby_llm/agents.rb +26 -0
- data/lib/ruby_llm-agents.rb +3 -0
- metadata +164 -0
data/README.md
ADDED
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
# RubyLLM::Agents
|
|
2
|
+
|
|
3
|
+
A powerful Rails engine for building, managing, and monitoring LLM-powered agents using [RubyLLM](https://github.com/crmne/ruby_llm).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **🤖 Agent DSL** - Declarative configuration for LLM agents with model, temperature, parameters, and caching
|
|
8
|
+
- **📊 Execution Tracking** - Automatic logging of all agent executions with token usage and costs
|
|
9
|
+
- **💰 Cost Analytics** - Track spending by agent, model, and time period with detailed breakdowns
|
|
10
|
+
- **📈 Dashboard UI** - Beautiful Turbo-powered dashboard for monitoring agents
|
|
11
|
+
- **⚡ Performance** - Built-in caching with configurable TTL and cache key versioning
|
|
12
|
+
- **🛠️ Generators** - Quickly scaffold new agents with customizable templates
|
|
13
|
+
- **🔍 Anomaly Detection** - Automatic warnings for unusual cost or duration patterns
|
|
14
|
+
- **🎯 Type Safety** - Structured output with RubyLLM::Schema integration
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- **Ruby**: >= 3.1.0
|
|
19
|
+
- **Rails**: >= 7.0
|
|
20
|
+
|
|
21
|
+
## Dependencies
|
|
22
|
+
|
|
23
|
+
The gem includes the following runtime dependencies:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
gem "rails", ">= 7.0"
|
|
27
|
+
gem "ruby_llm", ">= 1.0" # LLM client library
|
|
28
|
+
gem "turbo-rails", ">= 1.0" # Hotwire Turbo for real-time UI
|
|
29
|
+
gem "stimulus-rails", ">= 1.0" # Hotwire Stimulus for JavaScript
|
|
30
|
+
gem "chartkick", ">= 5.0" # Beautiful charts for analytics
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
### 1. Add to your Gemfile
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
gem "ruby_llm-agents"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then run:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bundle install
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Run the install generator
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
rails generate ruby_llm_agents:install
|
|
51
|
+
rails db:migrate
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This will:
|
|
55
|
+
|
|
56
|
+
- Create the `ruby_llm_agents_executions` table for execution tracking
|
|
57
|
+
- Add an initializer at `config/initializers/ruby_llm_agents.rb`
|
|
58
|
+
- Create `app/agents/application_agent.rb` as the base class for your agents
|
|
59
|
+
- Mount the dashboard at `/agents` in your routes
|
|
60
|
+
|
|
61
|
+
### 3. Configure your LLM provider
|
|
62
|
+
|
|
63
|
+
Set up your API keys for the LLM providers you want to use:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# .env or Rails credentials
|
|
67
|
+
GOOGLE_API_KEY=your_key_here
|
|
68
|
+
OPENAI_API_KEY=your_key_here
|
|
69
|
+
ANTHROPIC_API_KEY=your_key_here
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quick Start
|
|
73
|
+
|
|
74
|
+
### Creating Your First Agent
|
|
75
|
+
|
|
76
|
+
Use the generator to create a new agent:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
rails generate ruby_llm_agents:agent SearchIntent query:required limit:10
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This creates `app/agents/search_intent_agent.rb`:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
class SearchIntentAgent < ApplicationAgent
|
|
86
|
+
model "gemini-2.0-flash"
|
|
87
|
+
temperature 0.0
|
|
88
|
+
version "1.0"
|
|
89
|
+
|
|
90
|
+
param :query, required: true
|
|
91
|
+
param :limit, default: 10
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def system_prompt
|
|
96
|
+
<<~PROMPT
|
|
97
|
+
You are a search assistant that parses user queries
|
|
98
|
+
and extracts structured search filters.
|
|
99
|
+
PROMPT
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def user_prompt
|
|
103
|
+
query
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def schema
|
|
107
|
+
@schema ||= RubyLLM::Schema.create do
|
|
108
|
+
string :refined_query, description: "Cleaned and refined search query"
|
|
109
|
+
array :filters, of: :string, description: "Extracted search filters"
|
|
110
|
+
integer :category_id, description: "Detected product category", nullable: true
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Calling the Agent
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
# Basic call
|
|
120
|
+
result = SearchIntentAgent.call(query: "red summer dress under $50")
|
|
121
|
+
# => {
|
|
122
|
+
# refined_query: "red summer dress",
|
|
123
|
+
# filters: ["color:red", "season:summer", "price:<50"],
|
|
124
|
+
# category_id: 42
|
|
125
|
+
# }
|
|
126
|
+
|
|
127
|
+
# With custom parameters
|
|
128
|
+
result = SearchIntentAgent.call(
|
|
129
|
+
query: "blue jeans",
|
|
130
|
+
limit: 20
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Debug mode (no API call, shows prompt)
|
|
134
|
+
SearchIntentAgent.call(query: "test", dry_run: true)
|
|
135
|
+
# => {
|
|
136
|
+
# dry_run: true,
|
|
137
|
+
# agent: "SearchIntentAgent",
|
|
138
|
+
# model: "gemini-2.0-flash",
|
|
139
|
+
# temperature: 0.0,
|
|
140
|
+
# system_prompt: "You are a search assistant...",
|
|
141
|
+
# user_prompt: "test",
|
|
142
|
+
# schema: "RubyLLM::Schema"
|
|
143
|
+
# }
|
|
144
|
+
|
|
145
|
+
# Skip cache
|
|
146
|
+
SearchIntentAgent.call(query: "test", skip_cache: true)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Usage Guide
|
|
150
|
+
|
|
151
|
+
### Agent DSL
|
|
152
|
+
|
|
153
|
+
#### Model Configuration
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
class MyAgent < ApplicationAgent
|
|
157
|
+
# LLM model to use
|
|
158
|
+
model "gpt-4o" # OpenAI GPT-4
|
|
159
|
+
# model "claude-3-5-sonnet" # Anthropic Claude
|
|
160
|
+
# model "gemini-2.0-flash" # Google Gemini (default)
|
|
161
|
+
|
|
162
|
+
# Randomness (0.0 = deterministic, 1.0 = creative)
|
|
163
|
+
temperature 0.7
|
|
164
|
+
|
|
165
|
+
# Version for cache key generation
|
|
166
|
+
version "2.0"
|
|
167
|
+
|
|
168
|
+
# Request timeout in seconds
|
|
169
|
+
timeout 30
|
|
170
|
+
|
|
171
|
+
# Enable caching with TTL
|
|
172
|
+
cache 1.hour
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### Parameter Definition
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
class ProductSearchAgent < ApplicationAgent
|
|
180
|
+
# Required parameter - raises ArgumentError if not provided
|
|
181
|
+
param :query, required: true
|
|
182
|
+
|
|
183
|
+
# Optional parameter with default value
|
|
184
|
+
param :limit, default: 10
|
|
185
|
+
|
|
186
|
+
# Optional parameter (no default)
|
|
187
|
+
param :filters
|
|
188
|
+
|
|
189
|
+
# Multiple required parameters
|
|
190
|
+
param :user_id, required: true
|
|
191
|
+
param :session_id, required: true
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### Prompt Methods
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
class ContentGeneratorAgent < ApplicationAgent
|
|
199
|
+
param :topic, required: true
|
|
200
|
+
param :tone, default: "professional"
|
|
201
|
+
param :word_count, default: 500
|
|
202
|
+
|
|
203
|
+
private
|
|
204
|
+
|
|
205
|
+
# System prompt (optional) - sets the AI's role and instructions
|
|
206
|
+
def system_prompt
|
|
207
|
+
<<~PROMPT
|
|
208
|
+
You are a professional content writer specializing in #{topic}.
|
|
209
|
+
Write in a #{tone} tone.
|
|
210
|
+
PROMPT
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# User prompt (required) - the main request to the AI
|
|
214
|
+
def user_prompt
|
|
215
|
+
<<~PROMPT
|
|
216
|
+
Write a #{word_count}-word article about: #{topic}
|
|
217
|
+
|
|
218
|
+
Requirements:
|
|
219
|
+
- Clear structure with introduction, body, and conclusion
|
|
220
|
+
- Use examples and data where relevant
|
|
221
|
+
- Maintain a #{tone} tone throughout
|
|
222
|
+
PROMPT
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
#### Structured Output with Schema
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
class EmailClassifierAgent < ApplicationAgent
|
|
231
|
+
param :email_content, required: true
|
|
232
|
+
|
|
233
|
+
private
|
|
234
|
+
|
|
235
|
+
def system_prompt
|
|
236
|
+
"You are an email classification system. Analyze emails and categorize them."
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def user_prompt
|
|
240
|
+
email_content
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def schema
|
|
244
|
+
@schema ||= RubyLLM::Schema.create do
|
|
245
|
+
string :category,
|
|
246
|
+
enum: ["urgent", "important", "spam", "newsletter", "personal"],
|
|
247
|
+
description: "Email category"
|
|
248
|
+
|
|
249
|
+
number :priority,
|
|
250
|
+
description: "Priority score from 0 to 10"
|
|
251
|
+
|
|
252
|
+
array :tags,
|
|
253
|
+
of: :string,
|
|
254
|
+
description: "Relevant tags for the email"
|
|
255
|
+
|
|
256
|
+
boolean :requires_response,
|
|
257
|
+
description: "Whether the email requires a response"
|
|
258
|
+
|
|
259
|
+
object :sender_info do
|
|
260
|
+
string :name, nullable: true
|
|
261
|
+
string :company, nullable: true
|
|
262
|
+
boolean :is_known_contact
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
#### Response Processing
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
class DataExtractorAgent < ApplicationAgent
|
|
273
|
+
param :text, required: true
|
|
274
|
+
|
|
275
|
+
private
|
|
276
|
+
|
|
277
|
+
def user_prompt
|
|
278
|
+
"Extract key information from: #{text}"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def schema
|
|
282
|
+
@schema ||= RubyLLM::Schema.create do
|
|
283
|
+
string :summary
|
|
284
|
+
array :entities, of: :string
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Post-process the LLM response
|
|
289
|
+
def process_response(response)
|
|
290
|
+
result = super(response)
|
|
291
|
+
|
|
292
|
+
# Add custom processing
|
|
293
|
+
result[:entities] = result[:entities].map(&:downcase).uniq
|
|
294
|
+
result[:word_count] = result[:summary].split.length
|
|
295
|
+
result[:extracted_at] = Time.current
|
|
296
|
+
|
|
297
|
+
result
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
#### Custom Metadata
|
|
303
|
+
|
|
304
|
+
Add custom data to execution logs for filtering and analytics:
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
class UserQueryAgent < ApplicationAgent
|
|
308
|
+
param :query, required: true
|
|
309
|
+
param :user_id, required: true
|
|
310
|
+
param :source, default: "web"
|
|
311
|
+
|
|
312
|
+
# This data will be stored in the execution record's metadata column
|
|
313
|
+
def execution_metadata
|
|
314
|
+
{
|
|
315
|
+
user_id: user_id,
|
|
316
|
+
source: source,
|
|
317
|
+
query_length: query.length,
|
|
318
|
+
timestamp: Time.current.iso8601
|
|
319
|
+
}
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
private
|
|
323
|
+
|
|
324
|
+
def user_prompt
|
|
325
|
+
query
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Advanced Examples
|
|
331
|
+
|
|
332
|
+
#### Multi-Step Agent with Conversation History
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
class ConversationAgent < ApplicationAgent
|
|
336
|
+
param :messages, required: true # Array of {role:, content:} hashes
|
|
337
|
+
param :context, default: {}
|
|
338
|
+
|
|
339
|
+
def call
|
|
340
|
+
return dry_run_response if @options[:dry_run]
|
|
341
|
+
|
|
342
|
+
instrument_execution do
|
|
343
|
+
Timeout.timeout(self.class.timeout) do
|
|
344
|
+
client = build_client_with_messages(messages)
|
|
345
|
+
response = client.ask(user_prompt)
|
|
346
|
+
process_response(capture_response(response))
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
private
|
|
352
|
+
|
|
353
|
+
def system_prompt
|
|
354
|
+
"You are a helpful assistant. Remember the conversation context."
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def user_prompt
|
|
358
|
+
messages.last[:content]
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Usage
|
|
363
|
+
ConversationAgent.call(
|
|
364
|
+
messages: [
|
|
365
|
+
{ role: "user", content: "What's the weather like?" },
|
|
366
|
+
{ role: "assistant", content: "I don't have real-time weather data." },
|
|
367
|
+
{ role: "user", content: "Okay, tell me a joke then." }
|
|
368
|
+
]
|
|
369
|
+
)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
#### Agent with Custom Cache Key
|
|
373
|
+
|
|
374
|
+
```ruby
|
|
375
|
+
class RecommendationAgent < ApplicationAgent
|
|
376
|
+
param :user_id, required: true
|
|
377
|
+
param :category, required: true
|
|
378
|
+
param :limit, default: 10
|
|
379
|
+
|
|
380
|
+
cache 30.minutes
|
|
381
|
+
|
|
382
|
+
private
|
|
383
|
+
|
|
384
|
+
# Customize what goes into the cache key
|
|
385
|
+
# This excludes 'limit' from cache key, so different limits
|
|
386
|
+
# will return the same cached result
|
|
387
|
+
def cache_key_data
|
|
388
|
+
{ user_id: user_id, category: category }
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def user_prompt
|
|
392
|
+
"Generate #{limit} recommendations for user #{user_id} in category #{category}"
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Configuration
|
|
398
|
+
|
|
399
|
+
Edit `config/initializers/ruby_llm_agents.rb`:
|
|
400
|
+
|
|
401
|
+
```ruby
|
|
402
|
+
RubyLLM::Agents.configure do |config|
|
|
403
|
+
# ============================================================================
|
|
404
|
+
# Default Settings for All Agents
|
|
405
|
+
# ============================================================================
|
|
406
|
+
|
|
407
|
+
# Default LLM model (can be overridden per agent)
|
|
408
|
+
config.default_model = "gemini-2.0-flash"
|
|
409
|
+
|
|
410
|
+
# Default temperature (0.0 = deterministic, 1.0 = creative)
|
|
411
|
+
config.default_temperature = 0.0
|
|
412
|
+
|
|
413
|
+
# Default timeout for LLM requests (in seconds)
|
|
414
|
+
config.default_timeout = 60
|
|
415
|
+
|
|
416
|
+
# ============================================================================
|
|
417
|
+
# Caching Configuration
|
|
418
|
+
# ============================================================================
|
|
419
|
+
|
|
420
|
+
# Cache store for agent responses (default: Rails.cache)
|
|
421
|
+
config.cache_store = Rails.cache
|
|
422
|
+
# config.cache_store = ActiveSupport::Cache::MemoryStore.new
|
|
423
|
+
# config.cache_store = ActiveSupport::Cache::RedisCacheStore.new(url: ENV['REDIS_URL'])
|
|
424
|
+
|
|
425
|
+
# ============================================================================
|
|
426
|
+
# Execution Logging
|
|
427
|
+
# ============================================================================
|
|
428
|
+
|
|
429
|
+
# Use background job for logging (recommended for production)
|
|
430
|
+
config.async_logging = true
|
|
431
|
+
|
|
432
|
+
# How long to retain execution records (for cleanup tasks)
|
|
433
|
+
config.retention_period = 30.days
|
|
434
|
+
|
|
435
|
+
# ============================================================================
|
|
436
|
+
# Anomaly Detection
|
|
437
|
+
# ============================================================================
|
|
438
|
+
|
|
439
|
+
# Log warning if an execution costs more than this (in dollars)
|
|
440
|
+
config.anomaly_cost_threshold = 5.00
|
|
441
|
+
|
|
442
|
+
# Log warning if an execution takes longer than this (in milliseconds)
|
|
443
|
+
config.anomaly_duration_threshold = 10_000 # 10 seconds
|
|
444
|
+
|
|
445
|
+
# ============================================================================
|
|
446
|
+
# Dashboard Configuration
|
|
447
|
+
# ============================================================================
|
|
448
|
+
|
|
449
|
+
# Authentication for dashboard access
|
|
450
|
+
# Return true to allow access, false to deny
|
|
451
|
+
config.dashboard_auth = ->(controller) {
|
|
452
|
+
controller.current_user&.admin?
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
# Customize the parent controller for dashboard
|
|
456
|
+
config.dashboard_parent_controller = "ApplicationController"
|
|
457
|
+
end
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## Dashboard
|
|
461
|
+
|
|
462
|
+
### Mounting the Dashboard
|
|
463
|
+
|
|
464
|
+
The install generator automatically mounts the dashboard, but you can customize the path:
|
|
465
|
+
|
|
466
|
+
```ruby
|
|
467
|
+
# config/routes.rb
|
|
468
|
+
mount RubyLLM::Agents::Engine => "/agents"
|
|
469
|
+
# or
|
|
470
|
+
mount RubyLLM::Agents::Engine => "/admin/ai-agents", as: "agents_dashboard"
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Dashboard Features
|
|
474
|
+
|
|
475
|
+
The dashboard provides:
|
|
476
|
+
|
|
477
|
+
1. **Overview Page** (`/agents`)
|
|
478
|
+
- Today's execution stats (total, success rate, failures)
|
|
479
|
+
- Real-time cost tracking
|
|
480
|
+
- Performance trends (7-day chart)
|
|
481
|
+
- Top agents by usage
|
|
482
|
+
|
|
483
|
+
2. **Executions List** (`/agents/executions`)
|
|
484
|
+
- Filterable by agent type, status, date range
|
|
485
|
+
- Sortable by cost, duration, timestamp
|
|
486
|
+
- Real-time updates via Turbo Streams
|
|
487
|
+
- Search by parameters
|
|
488
|
+
|
|
489
|
+
3. **Execution Detail** (`/agents/executions/:id`)
|
|
490
|
+
- Full system and user prompts
|
|
491
|
+
- Complete LLM response
|
|
492
|
+
- Token usage breakdown (input, output, cached)
|
|
493
|
+
- Cost calculation
|
|
494
|
+
- Execution metadata
|
|
495
|
+
- Error details (if failed)
|
|
496
|
+
|
|
497
|
+
### Authentication
|
|
498
|
+
|
|
499
|
+
Protect your dashboard by configuring authentication:
|
|
500
|
+
|
|
501
|
+
```ruby
|
|
502
|
+
# config/initializers/ruby_llm_agents.rb
|
|
503
|
+
RubyLLM::Agents.configure do |config|
|
|
504
|
+
config.dashboard_auth = ->(controller) {
|
|
505
|
+
# Example: Devise authentication
|
|
506
|
+
controller.authenticate_user! && controller.current_user.admin?
|
|
507
|
+
|
|
508
|
+
# Example: Basic auth
|
|
509
|
+
# controller.authenticate_or_request_with_http_basic do |username, password|
|
|
510
|
+
# username == ENV['DASHBOARD_USERNAME'] &&
|
|
511
|
+
# password == ENV['DASHBOARD_PASSWORD']
|
|
512
|
+
# end
|
|
513
|
+
|
|
514
|
+
# Example: IP whitelist
|
|
515
|
+
# ['127.0.0.1', '::1'].include?(controller.request.remote_ip)
|
|
516
|
+
}
|
|
517
|
+
end
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
## Analytics & Reporting
|
|
521
|
+
|
|
522
|
+
Query execution data programmatically:
|
|
523
|
+
|
|
524
|
+
### Daily Reports
|
|
525
|
+
|
|
526
|
+
```ruby
|
|
527
|
+
# Get today's summary
|
|
528
|
+
report = RubyLLM::Agents::Execution.daily_report
|
|
529
|
+
# => {
|
|
530
|
+
# total_executions: 1250,
|
|
531
|
+
# successful: 1180,
|
|
532
|
+
# failed: 70,
|
|
533
|
+
# success_rate: 94.4,
|
|
534
|
+
# total_cost: 12.45,
|
|
535
|
+
# avg_duration_ms: 850,
|
|
536
|
+
# total_tokens: 450000
|
|
537
|
+
# }
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Cost Analysis
|
|
541
|
+
|
|
542
|
+
```ruby
|
|
543
|
+
# Cost breakdown by agent for this week
|
|
544
|
+
costs = RubyLLM::Agents::Execution.cost_by_agent(period: :this_week)
|
|
545
|
+
# => [
|
|
546
|
+
# { agent_type: "SearchIntentAgent", total_cost: 5.67, executions: 450 },
|
|
547
|
+
# { agent_type: "ContentGeneratorAgent", total_cost: 3.21, executions: 120 }
|
|
548
|
+
# ]
|
|
549
|
+
|
|
550
|
+
# Cost breakdown by model
|
|
551
|
+
costs = RubyLLM::Agents::Execution.cost_by_model(period: :today)
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### Agent Statistics
|
|
555
|
+
|
|
556
|
+
```ruby
|
|
557
|
+
# Stats for a specific agent
|
|
558
|
+
stats = RubyLLM::Agents::Execution.stats_for("SearchIntentAgent", period: :today)
|
|
559
|
+
# => {
|
|
560
|
+
# total: 150,
|
|
561
|
+
# successful: 145,
|
|
562
|
+
# failed: 5,
|
|
563
|
+
# success_rate: 96.67,
|
|
564
|
+
# avg_cost: 0.012,
|
|
565
|
+
# total_cost: 1.80,
|
|
566
|
+
# avg_duration_ms: 450,
|
|
567
|
+
# total_tokens: 75000
|
|
568
|
+
# }
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### Version Comparison
|
|
572
|
+
|
|
573
|
+
```ruby
|
|
574
|
+
# Compare two versions of an agent
|
|
575
|
+
comparison = RubyLLM::Agents::Execution.compare_versions(
|
|
576
|
+
"SearchIntentAgent",
|
|
577
|
+
"1.0",
|
|
578
|
+
"2.0",
|
|
579
|
+
period: :this_week
|
|
580
|
+
)
|
|
581
|
+
# => {
|
|
582
|
+
# "1.0" => { total: 450, success_rate: 94.2, avg_cost: 0.015 },
|
|
583
|
+
# "2.0" => { total: 550, success_rate: 96.8, avg_cost: 0.012 }
|
|
584
|
+
# }
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Trend Analysis
|
|
588
|
+
|
|
589
|
+
```ruby
|
|
590
|
+
# 7-day trend for an agent
|
|
591
|
+
trend = RubyLLM::Agents::Execution.trend_analysis(
|
|
592
|
+
agent_type: "SearchIntentAgent",
|
|
593
|
+
days: 7
|
|
594
|
+
)
|
|
595
|
+
# => [
|
|
596
|
+
# { date: "2024-01-01", executions: 120, cost: 1.45, avg_duration: 450 },
|
|
597
|
+
# { date: "2024-01-02", executions: 135, cost: 1.62, avg_duration: 430 },
|
|
598
|
+
# ...
|
|
599
|
+
# ]
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Scopes
|
|
603
|
+
|
|
604
|
+
Chain scopes for complex queries:
|
|
605
|
+
|
|
606
|
+
```ruby
|
|
607
|
+
# All successful executions today
|
|
608
|
+
RubyLLM::Agents::Execution.today.successful
|
|
609
|
+
|
|
610
|
+
# Failed executions for specific agent
|
|
611
|
+
RubyLLM::Agents::Execution.by_agent("SearchIntentAgent").failed
|
|
612
|
+
|
|
613
|
+
# Expensive executions this week
|
|
614
|
+
RubyLLM::Agents::Execution.this_week.expensive(1.00) # cost > $1
|
|
615
|
+
|
|
616
|
+
# Slow executions
|
|
617
|
+
RubyLLM::Agents::Execution.slow(5000) # duration > 5 seconds
|
|
618
|
+
|
|
619
|
+
# Complex query
|
|
620
|
+
expensive_slow_failures = RubyLLM::Agents::Execution
|
|
621
|
+
.this_week
|
|
622
|
+
.by_agent("ContentGeneratorAgent")
|
|
623
|
+
.failed
|
|
624
|
+
.expensive(0.50)
|
|
625
|
+
.slow(3000)
|
|
626
|
+
.order(created_at: :desc)
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
### Available Scopes
|
|
630
|
+
|
|
631
|
+
```ruby
|
|
632
|
+
# Time-based
|
|
633
|
+
.today
|
|
634
|
+
.this_week
|
|
635
|
+
.this_month
|
|
636
|
+
.yesterday
|
|
637
|
+
|
|
638
|
+
# Status
|
|
639
|
+
.successful
|
|
640
|
+
.failed
|
|
641
|
+
.status_error
|
|
642
|
+
.status_timeout
|
|
643
|
+
.status_running
|
|
644
|
+
|
|
645
|
+
# Agent/Model
|
|
646
|
+
.by_agent("AgentName")
|
|
647
|
+
.by_model("gpt-4o")
|
|
648
|
+
|
|
649
|
+
# Performance
|
|
650
|
+
.expensive(threshold) # cost > threshold
|
|
651
|
+
.slow(milliseconds) # duration > ms
|
|
652
|
+
|
|
653
|
+
# Token usage
|
|
654
|
+
.high_token_usage(threshold)
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
## Generators
|
|
658
|
+
|
|
659
|
+
### Agent Generator
|
|
660
|
+
|
|
661
|
+
```bash
|
|
662
|
+
# Basic agent
|
|
663
|
+
rails generate ruby_llm_agents:agent MyAgent
|
|
664
|
+
|
|
665
|
+
# Agent with parameters
|
|
666
|
+
rails generate ruby_llm_agents:agent SearchAgent query:required limit:10 filters
|
|
667
|
+
|
|
668
|
+
# Agent with custom model and temperature
|
|
669
|
+
rails generate ruby_llm_agents:agent ContentAgent \
|
|
670
|
+
topic:required \
|
|
671
|
+
--model=gpt-4o \
|
|
672
|
+
--temperature=0.7
|
|
673
|
+
|
|
674
|
+
# Agent with caching
|
|
675
|
+
rails generate ruby_llm_agents:agent CachedAgent \
|
|
676
|
+
key:required \
|
|
677
|
+
--cache=1.hour
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
### Install Generator
|
|
681
|
+
|
|
682
|
+
```bash
|
|
683
|
+
# Initial setup
|
|
684
|
+
rails generate ruby_llm_agents:install
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
### Upgrade Generator
|
|
688
|
+
|
|
689
|
+
```bash
|
|
690
|
+
# Upgrade to latest schema (when gem is updated)
|
|
691
|
+
rails generate ruby_llm_agents:upgrade
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
## Background Jobs
|
|
695
|
+
|
|
696
|
+
For production environments, enable async logging:
|
|
697
|
+
|
|
698
|
+
```ruby
|
|
699
|
+
# config/initializers/ruby_llm_agents.rb
|
|
700
|
+
RubyLLM::Agents.configure do |config|
|
|
701
|
+
config.async_logging = true
|
|
702
|
+
end
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
This uses `RubyLLM::Agents::ExecutionLoggerJob` to log executions in the background.
|
|
706
|
+
|
|
707
|
+
Make sure you have a job processor running:
|
|
708
|
+
|
|
709
|
+
```bash
|
|
710
|
+
# Using Solid Queue (Rails 7.1+)
|
|
711
|
+
bin/jobs
|
|
712
|
+
|
|
713
|
+
# Or Sidekiq
|
|
714
|
+
bundle exec sidekiq
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
## Maintenance Tasks
|
|
718
|
+
|
|
719
|
+
### Cleanup Old Executions
|
|
720
|
+
|
|
721
|
+
```ruby
|
|
722
|
+
# In a rake task or scheduled job
|
|
723
|
+
retention_period = RubyLLM::Agents.configuration.retention_period
|
|
724
|
+
RubyLLM::Agents::Execution.where("created_at < ?", retention_period.ago).delete_all
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### Export Data
|
|
728
|
+
|
|
729
|
+
```ruby
|
|
730
|
+
# Export to CSV
|
|
731
|
+
require 'csv'
|
|
732
|
+
|
|
733
|
+
CSV.open("agent_executions.csv", "wb") do |csv|
|
|
734
|
+
csv << ["Agent", "Status", "Cost", "Duration", "Timestamp"]
|
|
735
|
+
|
|
736
|
+
RubyLLM::Agents::Execution.this_month.find_each do |execution|
|
|
737
|
+
csv << [
|
|
738
|
+
execution.agent_type,
|
|
739
|
+
execution.status,
|
|
740
|
+
execution.total_cost,
|
|
741
|
+
execution.duration_ms,
|
|
742
|
+
execution.created_at
|
|
743
|
+
]
|
|
744
|
+
end
|
|
745
|
+
end
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
## Testing
|
|
749
|
+
|
|
750
|
+
### RSpec Example
|
|
751
|
+
|
|
752
|
+
```ruby
|
|
753
|
+
# spec/agents/search_intent_agent_spec.rb
|
|
754
|
+
require 'rails_helper'
|
|
755
|
+
|
|
756
|
+
RSpec.describe SearchIntentAgent do
|
|
757
|
+
describe ".call" do
|
|
758
|
+
it "extracts search intent from query" do
|
|
759
|
+
result = described_class.call(
|
|
760
|
+
query: "red summer dress under $50",
|
|
761
|
+
dry_run: true # Use dry_run for testing without API calls
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
expect(result[:dry_run]).to be true
|
|
765
|
+
expect(result[:agent]).to eq("SearchIntentAgent")
|
|
766
|
+
end
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
describe "parameter validation" do
|
|
770
|
+
it "requires query parameter" do
|
|
771
|
+
expect {
|
|
772
|
+
described_class.call(limit: 10)
|
|
773
|
+
}.to raise_error(ArgumentError, /missing required params/)
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
it "uses default limit" do
|
|
777
|
+
agent = described_class.new(query: "test")
|
|
778
|
+
expect(agent.limit).to eq(10)
|
|
779
|
+
end
|
|
780
|
+
end
|
|
781
|
+
end
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
### Mocking LLM Responses
|
|
785
|
+
|
|
786
|
+
```ruby
|
|
787
|
+
# spec/support/llm_helpers.rb
|
|
788
|
+
module LLMHelpers
|
|
789
|
+
def mock_llm_response(data)
|
|
790
|
+
response = instance_double(RubyLLM::Response, content: data)
|
|
791
|
+
allow_any_instance_of(RubyLLM::Chat).to receive(:ask).and_return(response)
|
|
792
|
+
end
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
# In your spec
|
|
796
|
+
RSpec.describe SearchIntentAgent do
|
|
797
|
+
include LLMHelpers
|
|
798
|
+
|
|
799
|
+
it "processes search intent" do
|
|
800
|
+
mock_llm_response({
|
|
801
|
+
refined_query: "red dress",
|
|
802
|
+
filters: ["color:red"],
|
|
803
|
+
category_id: 42
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
result = described_class.call(query: "red summer dress")
|
|
807
|
+
|
|
808
|
+
expect(result[:refined_query]).to eq("red dress")
|
|
809
|
+
expect(result[:filters]).to include("color:red")
|
|
810
|
+
end
|
|
811
|
+
end
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
## Development
|
|
815
|
+
|
|
816
|
+
After checking out the repo:
|
|
817
|
+
|
|
818
|
+
```bash
|
|
819
|
+
# Install dependencies
|
|
820
|
+
bin/setup
|
|
821
|
+
|
|
822
|
+
# Run tests
|
|
823
|
+
bundle exec rake spec
|
|
824
|
+
|
|
825
|
+
# Run linter
|
|
826
|
+
bundle exec standardrb
|
|
827
|
+
|
|
828
|
+
# Fix linting issues
|
|
829
|
+
bundle exec standardrb --fix
|
|
830
|
+
|
|
831
|
+
# Run console
|
|
832
|
+
bin/rails console
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
## Troubleshooting
|
|
836
|
+
|
|
837
|
+
### Agent execution fails with timeout
|
|
838
|
+
|
|
839
|
+
Increase timeout for specific agent:
|
|
840
|
+
|
|
841
|
+
```ruby
|
|
842
|
+
class SlowAgent < ApplicationAgent
|
|
843
|
+
timeout 120 # 2 minutes
|
|
844
|
+
end
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
### Cache not working
|
|
848
|
+
|
|
849
|
+
Ensure Rails cache is configured:
|
|
850
|
+
|
|
851
|
+
```ruby
|
|
852
|
+
# config/environments/production.rb
|
|
853
|
+
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
### Dashboard not accessible
|
|
857
|
+
|
|
858
|
+
Check route mounting and authentication:
|
|
859
|
+
|
|
860
|
+
```ruby
|
|
861
|
+
# config/routes.rb
|
|
862
|
+
mount RubyLLM::Agents::Engine => "/agents"
|
|
863
|
+
|
|
864
|
+
# config/initializers/ruby_llm_agents.rb
|
|
865
|
+
config.dashboard_auth = ->(controller) { true } # Allow all (dev only!)
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
### High costs
|
|
869
|
+
|
|
870
|
+
Monitor and set limits:
|
|
871
|
+
|
|
872
|
+
```ruby
|
|
873
|
+
# config/initializers/ruby_llm_agents.rb
|
|
874
|
+
config.anomaly_cost_threshold = 1.00 # Alert at $1
|
|
875
|
+
|
|
876
|
+
# Check expensive executions
|
|
877
|
+
RubyLLM::Agents::Execution.this_week.expensive(0.50)
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
## Contributing
|
|
881
|
+
|
|
882
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/adham90/ruby_llm-agents.
|
|
883
|
+
|
|
884
|
+
1. Fork the repository
|
|
885
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
886
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
887
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
888
|
+
5. Create a new Pull Request
|
|
889
|
+
|
|
890
|
+
## License
|
|
891
|
+
|
|
892
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
893
|
+
|
|
894
|
+
## Credits
|
|
895
|
+
|
|
896
|
+
Built with ❤️ by [Adham Eldeeb](https://github.com/adham90)
|
|
897
|
+
|
|
898
|
+
Powered by [RubyLLM](https://github.com/crmne/ruby_llm)
|