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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +898 -0
  4. data/app/channels/ruby_llm/agents/executions_channel.rb +23 -0
  5. data/app/controllers/ruby_llm/agents/agents_controller.rb +100 -0
  6. data/app/controllers/ruby_llm/agents/application_controller.rb +20 -0
  7. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +34 -0
  8. data/app/controllers/ruby_llm/agents/executions_controller.rb +93 -0
  9. data/app/helpers/ruby_llm/agents/application_helper.rb +149 -0
  10. data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +56 -0
  11. data/app/javascript/ruby_llm/agents/controllers/index.js +12 -0
  12. data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +83 -0
  13. data/app/models/ruby_llm/agents/execution/analytics.rb +166 -0
  14. data/app/models/ruby_llm/agents/execution/metrics.rb +89 -0
  15. data/app/models/ruby_llm/agents/execution/scopes.rb +81 -0
  16. data/app/models/ruby_llm/agents/execution.rb +81 -0
  17. data/app/services/ruby_llm/agents/agent_registry.rb +112 -0
  18. data/app/views/layouts/rubyllm/agents/application.html.erb +276 -0
  19. data/app/views/rubyllm/agents/agents/index.html.erb +89 -0
  20. data/app/views/rubyllm/agents/agents/show.html.erb +562 -0
  21. data/app/views/rubyllm/agents/dashboard/_execution_item.html.erb +48 -0
  22. data/app/views/rubyllm/agents/dashboard/index.html.erb +121 -0
  23. data/app/views/rubyllm/agents/executions/_execution.html.erb +64 -0
  24. data/app/views/rubyllm/agents/executions/_filters.html.erb +172 -0
  25. data/app/views/rubyllm/agents/executions/_list.html.erb +229 -0
  26. data/app/views/rubyllm/agents/executions/index.html.erb +83 -0
  27. data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +4 -0
  28. data/app/views/rubyllm/agents/executions/show.html.erb +240 -0
  29. data/app/views/rubyllm/agents/shared/_executions_table.html.erb +193 -0
  30. data/app/views/rubyllm/agents/shared/_stat_card.html.erb +14 -0
  31. data/app/views/rubyllm/agents/shared/_status_badge.html.erb +65 -0
  32. data/app/views/rubyllm/agents/shared/_status_dot.html.erb +18 -0
  33. data/config/routes.rb +13 -0
  34. data/lib/generators/ruby_llm_agents/agent_generator.rb +79 -0
  35. data/lib/generators/ruby_llm_agents/install_generator.rb +89 -0
  36. data/lib/generators/ruby_llm_agents/templates/add_prompts_migration.rb.tt +12 -0
  37. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +46 -0
  38. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +22 -0
  39. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +36 -0
  40. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +66 -0
  41. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +59 -0
  42. data/lib/ruby_llm/agents/base.rb +271 -0
  43. data/lib/ruby_llm/agents/configuration.rb +36 -0
  44. data/lib/ruby_llm/agents/engine.rb +32 -0
  45. data/lib/ruby_llm/agents/execution_logger_job.rb +59 -0
  46. data/lib/ruby_llm/agents/inflections.rb +13 -0
  47. data/lib/ruby_llm/agents/instrumentation.rb +245 -0
  48. data/lib/ruby_llm/agents/version.rb +7 -0
  49. data/lib/ruby_llm/agents.rb +26 -0
  50. data/lib/ruby_llm-agents.rb +3 -0
  51. 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)