ai-agents 0.1.2 → 0.2.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 +4 -4
- data/.claude/commands/bump-version.md +44 -0
- data/CHANGELOG.md +35 -0
- data/CLAUDE.md +59 -15
- data/README.md +29 -106
- data/docs/Gemfile +14 -0
- data/docs/Gemfile.lock +183 -0
- data/docs/_config.yml +53 -0
- data/docs/_sass/color_schemes/ruby.scss +72 -0
- data/docs/_sass/custom/custom.scss +93 -0
- data/docs/architecture.md +353 -0
- data/docs/assets/fonts/InterVariable.woff2 +0 -0
- data/docs/concepts/agent-tool.md +166 -0
- data/docs/concepts/agents.md +43 -0
- data/docs/concepts/callbacks.md +42 -0
- data/docs/concepts/context.md +110 -0
- data/docs/concepts/handoffs.md +81 -0
- data/docs/concepts/runner.md +87 -0
- data/docs/concepts/tools.md +62 -0
- data/docs/concepts.md +22 -0
- data/docs/guides/agent-as-tool-pattern.md +242 -0
- data/docs/guides/multi-agent-systems.md +261 -0
- data/docs/guides/rails-integration.md +440 -0
- data/docs/guides/state-persistence.md +451 -0
- data/docs/guides.md +18 -0
- data/docs/index.md +97 -0
- data/examples/collaborative-copilot/README.md +169 -0
- data/examples/collaborative-copilot/agents/analysis_agent.rb +48 -0
- data/examples/collaborative-copilot/agents/answer_suggestion_agent.rb +50 -0
- data/examples/collaborative-copilot/agents/copilot_orchestrator.rb +85 -0
- data/examples/collaborative-copilot/agents/integrations_agent.rb +58 -0
- data/examples/collaborative-copilot/agents/research_agent.rb +52 -0
- data/examples/collaborative-copilot/data/contacts.json +47 -0
- data/examples/collaborative-copilot/data/conversations.json +170 -0
- data/examples/collaborative-copilot/data/knowledge_base.json +58 -0
- data/examples/collaborative-copilot/data/linear_issues.json +83 -0
- data/examples/collaborative-copilot/data/stripe_billing.json +71 -0
- data/examples/collaborative-copilot/interactive.rb +90 -0
- data/examples/collaborative-copilot/tools/create_linear_ticket_tool.rb +58 -0
- data/examples/collaborative-copilot/tools/get_article_tool.rb +41 -0
- data/examples/collaborative-copilot/tools/get_contact_tool.rb +51 -0
- data/examples/collaborative-copilot/tools/get_conversation_tool.rb +53 -0
- data/examples/collaborative-copilot/tools/get_stripe_billing_tool.rb +44 -0
- data/examples/collaborative-copilot/tools/search_contacts_tool.rb +57 -0
- data/examples/collaborative-copilot/tools/search_conversations_tool.rb +54 -0
- data/examples/collaborative-copilot/tools/search_knowledge_base_tool.rb +55 -0
- data/examples/collaborative-copilot/tools/search_linear_issues_tool.rb +60 -0
- data/examples/isp-support/interactive.rb +43 -4
- data/lib/agents/agent.rb +34 -0
- data/lib/agents/agent_runner.rb +66 -1
- data/lib/agents/agent_tool.rb +113 -0
- data/lib/agents/callback_manager.rb +54 -0
- data/lib/agents/handoff.rb +8 -34
- data/lib/agents/message_extractor.rb +82 -0
- data/lib/agents/run_context.rb +5 -2
- data/lib/agents/runner.rb +16 -27
- data/lib/agents/tool_wrapper.rb +11 -1
- data/lib/agents/version.rb +1 -1
- data/lib/agents.rb +3 -0
- metadata +48 -1
@@ -0,0 +1,440 @@
|
|
1
|
+
---
|
2
|
+
layout: default
|
3
|
+
title: Rails Integration
|
4
|
+
parent: Guides
|
5
|
+
nav_order: 2
|
6
|
+
---
|
7
|
+
|
8
|
+
# Rails Integration
|
9
|
+
|
10
|
+
This guide covers integrating the AI Agents library with Ruby on Rails applications, including conversation persistence with ActiveRecord and session management.
|
11
|
+
|
12
|
+
## Setup
|
13
|
+
|
14
|
+
Add the gem to your Rails application:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
# Gemfile
|
18
|
+
gem 'ai-agents'
|
19
|
+
```
|
20
|
+
|
21
|
+
Configure your LLM providers in an initializer:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
# config/initializers/ai_agents.rb
|
25
|
+
Agents.configure do |config|
|
26
|
+
config.openai_api_key = Rails.application.credentials.openai_api_key
|
27
|
+
config.anthropic_api_key = Rails.application.credentials.anthropic_api_key
|
28
|
+
config.default_model = 'gpt-4o-mini'
|
29
|
+
config.debug = Rails.env.development?
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
## ActiveRecord Integration
|
34
|
+
|
35
|
+
### Conversation Persistence
|
36
|
+
|
37
|
+
Create a model to store conversation contexts:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
# Generate migration
|
41
|
+
rails generate model Conversation user:references context:text current_agent:string
|
42
|
+
|
43
|
+
# db/migrate/xxx_create_conversations.rb
|
44
|
+
class CreateConversations < ActiveRecord::Migration[7.0]
|
45
|
+
def change
|
46
|
+
create_table :conversations do |t|
|
47
|
+
t.references :user, null: false, foreign_key: true
|
48
|
+
t.text :context, null: false
|
49
|
+
t.string :current_agent
|
50
|
+
t.timestamps
|
51
|
+
end
|
52
|
+
|
53
|
+
add_index :conversations, [:user_id, :created_at]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
Define the Conversation model:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
# app/models/conversation.rb
|
62
|
+
class Conversation < ApplicationRecord
|
63
|
+
belongs_to :user
|
64
|
+
|
65
|
+
# Serialize context as JSON
|
66
|
+
serialize :context, JSON
|
67
|
+
|
68
|
+
validates :context, presence: true
|
69
|
+
|
70
|
+
def self.for_user(user)
|
71
|
+
where(user: user).order(:created_at)
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.latest_for_user(user)
|
75
|
+
for_user(user).last
|
76
|
+
end
|
77
|
+
|
78
|
+
# Convert to agent context hash
|
79
|
+
def to_agent_context
|
80
|
+
context.deep_symbolize_keys
|
81
|
+
end
|
82
|
+
|
83
|
+
# Create from agent result
|
84
|
+
def self.from_agent_result(user, result)
|
85
|
+
create!(
|
86
|
+
user: user,
|
87
|
+
context: result.context.to_h,
|
88
|
+
current_agent: result.context.current_agent_name
|
89
|
+
)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
### Session Management
|
95
|
+
|
96
|
+
Create a service to manage agent conversations:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
# app/services/agent_conversation_service.rb
|
100
|
+
class AgentConversationService
|
101
|
+
def initialize(user)
|
102
|
+
@user = user
|
103
|
+
@runner = create_agent_runner
|
104
|
+
end
|
105
|
+
|
106
|
+
def send_message(message)
|
107
|
+
# Get existing conversation context
|
108
|
+
context = load_conversation_context
|
109
|
+
|
110
|
+
# Run agent with message
|
111
|
+
result = @runner.run(message, context: context)
|
112
|
+
|
113
|
+
# Persist updated conversation
|
114
|
+
save_conversation(result)
|
115
|
+
|
116
|
+
result
|
117
|
+
end
|
118
|
+
|
119
|
+
def reset_conversation
|
120
|
+
Conversation.where(user: @user).destroy_all
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def create_agent_runner
|
126
|
+
# Create your agents here
|
127
|
+
triage_agent = Agents::Agent.new(
|
128
|
+
name: "Triage",
|
129
|
+
instructions: build_triage_instructions,
|
130
|
+
tools: [CustomerLookupTool.new]
|
131
|
+
)
|
132
|
+
|
133
|
+
billing_agent = Agents::Agent.new(
|
134
|
+
name: "Billing",
|
135
|
+
instructions: "Handle billing and payment inquiries.",
|
136
|
+
tools: [BillingTool.new, PaymentTool.new]
|
137
|
+
)
|
138
|
+
|
139
|
+
support_agent = Agents::Agent.new(
|
140
|
+
name: "Support",
|
141
|
+
instructions: "Provide technical support and troubleshooting.",
|
142
|
+
tools: [TechnicalTool.new]
|
143
|
+
)
|
144
|
+
|
145
|
+
triage_agent.register_handoffs(billing_agent, support_agent)
|
146
|
+
|
147
|
+
Agents::AgentRunner.with_agents(triage_agent, billing_agent, support_agent)
|
148
|
+
end
|
149
|
+
|
150
|
+
def build_triage_instructions
|
151
|
+
->(context) {
|
152
|
+
user_info = context[:user_info] || {}
|
153
|
+
|
154
|
+
<<~INSTRUCTIONS
|
155
|
+
You are a customer service triage agent for #{@user.name}.
|
156
|
+
|
157
|
+
Customer Details:
|
158
|
+
- Name: #{@user.name}
|
159
|
+
- Email: #{@user.email}
|
160
|
+
- Account Type: #{user_info[:account_type] || 'standard'}
|
161
|
+
|
162
|
+
Route customers to the appropriate department:
|
163
|
+
- Billing: Payment issues, account billing, refunds
|
164
|
+
- Support: Technical problems, product questions
|
165
|
+
|
166
|
+
Always be professional and helpful.
|
167
|
+
INSTRUCTIONS
|
168
|
+
}
|
169
|
+
end
|
170
|
+
|
171
|
+
def load_conversation_context
|
172
|
+
latest_conversation = Conversation.latest_for_user(@user)
|
173
|
+
return initial_context unless latest_conversation
|
174
|
+
|
175
|
+
latest_conversation.to_agent_context
|
176
|
+
end
|
177
|
+
|
178
|
+
def initial_context
|
179
|
+
{
|
180
|
+
user_id: @user.id,
|
181
|
+
user_info: {
|
182
|
+
name: @user.name,
|
183
|
+
email: @user.email,
|
184
|
+
account_type: @user.account_type
|
185
|
+
}
|
186
|
+
}
|
187
|
+
end
|
188
|
+
|
189
|
+
def save_conversation(result)
|
190
|
+
Conversation.from_agent_result(@user, result)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
```
|
194
|
+
|
195
|
+
## Controller Integration
|
196
|
+
|
197
|
+
Create a controller for handling agent conversations:
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
# app/controllers/agent_conversations_controller.rb
|
201
|
+
class AgentConversationsController < ApplicationController
|
202
|
+
before_action :authenticate_user!
|
203
|
+
|
204
|
+
def create
|
205
|
+
service = AgentConversationService.new(current_user)
|
206
|
+
|
207
|
+
begin
|
208
|
+
result = service.send_message(params[:message])
|
209
|
+
|
210
|
+
render json: {
|
211
|
+
response: result.output,
|
212
|
+
agent: result.context.current_agent_name,
|
213
|
+
conversation_id: result.context[:conversation_id]
|
214
|
+
}
|
215
|
+
rescue => e
|
216
|
+
Rails.logger.error "Agent conversation error: #{e.message}"
|
217
|
+
render json: { error: "Unable to process your request" }, status: 500
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def reset
|
222
|
+
service = AgentConversationService.new(current_user)
|
223
|
+
service.reset_conversation
|
224
|
+
|
225
|
+
render json: { message: "Conversation reset successfully" }
|
226
|
+
end
|
227
|
+
|
228
|
+
def history
|
229
|
+
conversations = Conversation.for_user(current_user)
|
230
|
+
.includes(:user)
|
231
|
+
.limit(50)
|
232
|
+
|
233
|
+
render json: conversations.map do |conv|
|
234
|
+
{
|
235
|
+
id: conv.id,
|
236
|
+
agent: conv.current_agent,
|
237
|
+
timestamp: conv.created_at,
|
238
|
+
context_keys: conv.context.keys
|
239
|
+
}
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
```
|
244
|
+
|
245
|
+
## Custom Rails Tools
|
246
|
+
|
247
|
+
Create Rails-specific tools for database operations:
|
248
|
+
|
249
|
+
```ruby
|
250
|
+
# app/tools/customer_lookup_tool.rb
|
251
|
+
class CustomerLookupTool < Agents::Tool
|
252
|
+
name "lookup_customer"
|
253
|
+
description "Look up customer information by email or ID"
|
254
|
+
param :identifier, type: "string", desc: "Email address or customer ID"
|
255
|
+
|
256
|
+
def perform(tool_context, identifier:)
|
257
|
+
# Access Rails models safely
|
258
|
+
customer = User.find_by(email: identifier) || User.find_by(id: identifier)
|
259
|
+
|
260
|
+
return "Customer not found" unless customer
|
261
|
+
|
262
|
+
{
|
263
|
+
name: customer.name,
|
264
|
+
email: customer.email,
|
265
|
+
account_type: customer.account_type,
|
266
|
+
created_at: customer.created_at,
|
267
|
+
last_login: customer.last_sign_in_at
|
268
|
+
}
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
# app/tools/billing_tool.rb
|
273
|
+
class BillingTool < Agents::Tool
|
274
|
+
name "get_billing_info"
|
275
|
+
description "Retrieve billing information for a customer"
|
276
|
+
param :user_id, type: "integer", desc: "Customer user ID"
|
277
|
+
|
278
|
+
def perform(tool_context, user_id:)
|
279
|
+
user = User.find(user_id)
|
280
|
+
billing_info = user.billing_profile
|
281
|
+
|
282
|
+
return "No billing information found" unless billing_info
|
283
|
+
|
284
|
+
{
|
285
|
+
plan: billing_info.plan_name,
|
286
|
+
status: billing_info.status,
|
287
|
+
next_billing_date: billing_info.next_billing_date,
|
288
|
+
amount: billing_info.monthly_amount
|
289
|
+
}
|
290
|
+
rescue ActiveRecord::RecordNotFound
|
291
|
+
"Customer not found"
|
292
|
+
end
|
293
|
+
end
|
294
|
+
```
|
295
|
+
|
296
|
+
## Background Processing
|
297
|
+
|
298
|
+
For longer conversations, use background jobs:
|
299
|
+
|
300
|
+
```ruby
|
301
|
+
# app/jobs/agent_conversation_job.rb
|
302
|
+
class AgentConversationJob < ApplicationJob
|
303
|
+
queue_as :default
|
304
|
+
|
305
|
+
def perform(user_id, message, conversation_id = nil)
|
306
|
+
user = User.find(user_id)
|
307
|
+
service = AgentConversationService.new(user)
|
308
|
+
|
309
|
+
result = service.send_message(message)
|
310
|
+
|
311
|
+
# Broadcast result via ActionCable
|
312
|
+
ActionCable.server.broadcast(
|
313
|
+
"agent_conversation_#{user_id}",
|
314
|
+
{
|
315
|
+
response: result.output,
|
316
|
+
agent: result.context.current_agent_name,
|
317
|
+
conversation_id: conversation_id
|
318
|
+
}
|
319
|
+
)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# Enqueue job from controller
|
324
|
+
def create_async
|
325
|
+
job_id = AgentConversationJob.perform_later(
|
326
|
+
current_user.id,
|
327
|
+
params[:message],
|
328
|
+
params[:conversation_id]
|
329
|
+
)
|
330
|
+
|
331
|
+
render json: { job_id: job_id }
|
332
|
+
end
|
333
|
+
```
|
334
|
+
|
335
|
+
## Error Handling
|
336
|
+
|
337
|
+
Implement comprehensive error handling:
|
338
|
+
|
339
|
+
```ruby
|
340
|
+
# app/services/agent_conversation_service.rb
|
341
|
+
class AgentConversationService
|
342
|
+
class AgentError < StandardError; end
|
343
|
+
class ContextError < StandardError; end
|
344
|
+
|
345
|
+
def send_message(message)
|
346
|
+
validate_message(message)
|
347
|
+
|
348
|
+
context = load_conversation_context
|
349
|
+
|
350
|
+
begin
|
351
|
+
result = @runner.run(message, context: context)
|
352
|
+
save_conversation(result)
|
353
|
+
result
|
354
|
+
rescue RubyLLM::Error => e
|
355
|
+
Rails.logger.error "LLM Error: #{e.message}"
|
356
|
+
raise AgentError, "AI service temporarily unavailable"
|
357
|
+
rescue JSON::ParserError => e
|
358
|
+
Rails.logger.error "Context parsing error: #{e.message}"
|
359
|
+
raise ContextError, "Conversation context corrupted"
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
private
|
364
|
+
|
365
|
+
def validate_message(message)
|
366
|
+
raise ArgumentError, "Message cannot be blank" if message.blank?
|
367
|
+
raise ArgumentError, "Message too long" if message.length > 5000
|
368
|
+
end
|
369
|
+
end
|
370
|
+
```
|
371
|
+
|
372
|
+
## Testing
|
373
|
+
|
374
|
+
Test Rails integration with RSpec:
|
375
|
+
|
376
|
+
```ruby
|
377
|
+
# spec/services/agent_conversation_service_spec.rb
|
378
|
+
RSpec.describe AgentConversationService do
|
379
|
+
let(:user) { create(:user) }
|
380
|
+
let(:service) { described_class.new(user) }
|
381
|
+
|
382
|
+
describe '#send_message' do
|
383
|
+
it 'creates a conversation record' do
|
384
|
+
expect {
|
385
|
+
service.send_message("Hello")
|
386
|
+
}.to change(Conversation, :count).by(1)
|
387
|
+
end
|
388
|
+
|
389
|
+
it 'persists context correctly' do
|
390
|
+
result = service.send_message("Hello")
|
391
|
+
conversation = Conversation.last
|
392
|
+
|
393
|
+
expect(conversation.user).to eq(user)
|
394
|
+
expect(conversation.context).to include('user_id' => user.id)
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
describe '#reset_conversation' do
|
399
|
+
before { service.send_message("Hello") }
|
400
|
+
|
401
|
+
it 'destroys all conversations for user' do
|
402
|
+
expect {
|
403
|
+
service.reset_conversation
|
404
|
+
}.to change(Conversation, :count).by(-1)
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
```
|
409
|
+
|
410
|
+
## Deployment Considerations
|
411
|
+
|
412
|
+
### Environment Variables
|
413
|
+
|
414
|
+
```ruby
|
415
|
+
# config/credentials.yml.enc
|
416
|
+
openai_api_key: your_openai_key
|
417
|
+
anthropic_api_key: your_anthropic_key
|
418
|
+
|
419
|
+
# Or use environment variables
|
420
|
+
ENV['OPENAI_API_KEY']
|
421
|
+
ENV['ANTHROPIC_API_KEY']
|
422
|
+
```
|
423
|
+
|
424
|
+
### Database Indexing
|
425
|
+
|
426
|
+
```ruby
|
427
|
+
# Add indexes for better query performance
|
428
|
+
add_index :conversations, [:user_id, :current_agent]
|
429
|
+
add_index :conversations, :created_at
|
430
|
+
```
|
431
|
+
|
432
|
+
### Memory Management
|
433
|
+
|
434
|
+
```ruby
|
435
|
+
# Cleanup old conversations
|
436
|
+
# config/schedule.rb (whenever gem)
|
437
|
+
every 1.day, at: '2:00 am' do
|
438
|
+
runner "Conversation.where('created_at < ?', 30.days.ago).destroy_all"
|
439
|
+
end
|
440
|
+
```
|