ai-agents 0.1.1 → 0.1.3
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 +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/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 +21 -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 +95 -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/agents_factory.rb +57 -1
- data/examples/isp-support/tools/create_lead_tool.rb +16 -2
- data/examples/isp-support/tools/crm_lookup_tool.rb +13 -1
- data/lib/agents/agent.rb +52 -6
- data/lib/agents/agent_tool.rb +113 -0
- data/lib/agents/handoff.rb +8 -34
- data/lib/agents/tool_context.rb +36 -0
- data/lib/agents/version.rb +1 -1
- data/lib/agents.rb +1 -0
- metadata +44 -2
@@ -0,0 +1,261 @@
|
|
1
|
+
---
|
2
|
+
layout: default
|
3
|
+
title: Building Multi-Agent Systems
|
4
|
+
parent: Guides
|
5
|
+
nav_order: 1
|
6
|
+
---
|
7
|
+
|
8
|
+
# Building Multi-Agent Systems
|
9
|
+
|
10
|
+
Multi-agent systems allow you to decompose complex problems into specialized agents that collaborate to provide comprehensive solutions. This guide covers patterns and best practices for designing effective multi-agent workflows.
|
11
|
+
|
12
|
+
## Core Patterns
|
13
|
+
|
14
|
+
### Hub-and-Spoke Architecture
|
15
|
+
|
16
|
+
The most common and stable pattern is hub-and-spoke, where a central triage agent routes conversations to specialized agents:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
# Specialized agents
|
20
|
+
billing_agent = Agents::Agent.new(
|
21
|
+
name: "Billing",
|
22
|
+
instructions: "Handle billing inquiries, payment processing, and account issues."
|
23
|
+
)
|
24
|
+
|
25
|
+
support_agent = Agents::Agent.new(
|
26
|
+
name: "Support",
|
27
|
+
instructions: "Provide technical troubleshooting and product support."
|
28
|
+
)
|
29
|
+
|
30
|
+
# Central hub agent
|
31
|
+
triage_agent = Agents::Agent.new(
|
32
|
+
name: "Triage",
|
33
|
+
instructions: "Route users to the appropriate specialist. Only hand off, don't solve problems yourself."
|
34
|
+
)
|
35
|
+
|
36
|
+
# Register handoffs (one-way: triage -> specialists)
|
37
|
+
triage_agent.register_handoffs(billing_agent, support_agent)
|
38
|
+
|
39
|
+
# Create runner with triage as entry point
|
40
|
+
runner = Agents::AgentRunner.with_agents(triage_agent, billing_agent, support_agent)
|
41
|
+
```
|
42
|
+
|
43
|
+
### Dynamic Instructions
|
44
|
+
|
45
|
+
Use Proc-based instructions to create context-aware agents:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
support_agent = Agents::Agent.new(
|
49
|
+
name: "Support",
|
50
|
+
instructions: ->(context) {
|
51
|
+
customer_tier = context[:customer_tier] || "standard"
|
52
|
+
<<~INSTRUCTIONS
|
53
|
+
You are a technical support specialist for #{customer_tier} tier customers.
|
54
|
+
#{customer_tier == "premium" ? "Provide priority white-glove service." : ""}
|
55
|
+
|
56
|
+
Available tools: diagnostics, escalation
|
57
|
+
INSTRUCTIONS
|
58
|
+
}
|
59
|
+
)
|
60
|
+
```
|
61
|
+
|
62
|
+
## Agent Design Principles
|
63
|
+
|
64
|
+
### Clear Boundaries
|
65
|
+
|
66
|
+
Each agent should have a distinct, non-overlapping responsibility:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
# GOOD: Clear specialization
|
70
|
+
sales_agent = Agents::Agent.new(
|
71
|
+
name: "Sales",
|
72
|
+
instructions: "Handle product inquiries, pricing, and purchase decisions. Transfer technical questions to support."
|
73
|
+
)
|
74
|
+
|
75
|
+
support_agent = Agents::Agent.new(
|
76
|
+
name: "Support",
|
77
|
+
instructions: "Handle technical issues and product troubleshooting. Transfer sales questions to sales team."
|
78
|
+
)
|
79
|
+
```
|
80
|
+
|
81
|
+
### Avoiding Handoff Loops
|
82
|
+
|
83
|
+
Design instructions to prevent infinite handoffs:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
# BAD: Conflicting handoff criteria
|
87
|
+
agent_a = Agents::Agent.new(
|
88
|
+
instructions: "Handle X. If you need Y info, hand off to Agent B."
|
89
|
+
)
|
90
|
+
agent_b = Agents::Agent.new(
|
91
|
+
instructions: "Handle Y. If you need X context, hand off to Agent A."
|
92
|
+
)
|
93
|
+
|
94
|
+
# GOOD: Clear escalation hierarchy
|
95
|
+
specialist = Agents::Agent.new(
|
96
|
+
instructions: "Handle specialized requests. Ask users for needed info directly."
|
97
|
+
)
|
98
|
+
triage = Agents::Agent.new(
|
99
|
+
instructions: "Route users to specialists. Don't solve problems yourself."
|
100
|
+
)
|
101
|
+
```
|
102
|
+
|
103
|
+
## Conversation Flow Management
|
104
|
+
|
105
|
+
### Entry Points
|
106
|
+
|
107
|
+
The first agent in `AgentRunner.with_agents()` becomes the default entry point:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
# Triage agent handles all initial conversations
|
111
|
+
runner = Agents::AgentRunner.with_agents(triage_agent, billing_agent, support_agent)
|
112
|
+
|
113
|
+
# Start conversation
|
114
|
+
result = runner.run("I need help with my account")
|
115
|
+
# -> Automatically starts with triage_agent
|
116
|
+
```
|
117
|
+
|
118
|
+
### Context Preservation
|
119
|
+
|
120
|
+
The AgentRunner automatically maintains conversation history across handoffs:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
# First interaction
|
124
|
+
result1 = runner.run("My name is John and I have a billing question")
|
125
|
+
# -> Triage agent hands off to billing agent
|
126
|
+
|
127
|
+
# Continue conversation
|
128
|
+
result2 = runner.run("What payment methods do you accept?", context: result1.context)
|
129
|
+
# -> Billing agent remembers John's name and previous context
|
130
|
+
```
|
131
|
+
|
132
|
+
### Handoff Detection
|
133
|
+
|
134
|
+
The system automatically determines the current agent from conversation history:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
# No need to manually specify which agent to use
|
138
|
+
result = runner.run("Follow up question", context: previous_result.context)
|
139
|
+
# -> AgentRunner automatically selects correct agent based on conversation state
|
140
|
+
```
|
141
|
+
|
142
|
+
## Advanced Patterns
|
143
|
+
|
144
|
+
### Tool-Specific Agents
|
145
|
+
|
146
|
+
Create agents specialized for particular tool categories:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
data_agent = Agents::Agent.new(
|
150
|
+
name: "DataAnalyst",
|
151
|
+
instructions: "Analyze data and generate reports using available analytics tools.",
|
152
|
+
tools: [DatabaseTool.new, ChartGeneratorTool.new, ReportTool.new]
|
153
|
+
)
|
154
|
+
|
155
|
+
communication_agent = Agents::Agent.new(
|
156
|
+
name: "Communications",
|
157
|
+
instructions: "Handle notifications and external communications.",
|
158
|
+
tools: [EmailTool.new, SlackTool.new, SMSTool.new]
|
159
|
+
)
|
160
|
+
```
|
161
|
+
|
162
|
+
### Conditional Handoffs
|
163
|
+
|
164
|
+
Use dynamic instructions to control handoff behavior:
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
triage_agent = Agents::Agent.new(
|
168
|
+
name: "Triage",
|
169
|
+
instructions: ->(context) {
|
170
|
+
business_hours = context[:business_hours] || false
|
171
|
+
|
172
|
+
base_instructions = "Route users to appropriate departments."
|
173
|
+
|
174
|
+
if business_hours
|
175
|
+
base_instructions + " All departments are available."
|
176
|
+
else
|
177
|
+
base_instructions + " Only hand off urgent technical issues to support. Others should wait for business hours."
|
178
|
+
end
|
179
|
+
}
|
180
|
+
)
|
181
|
+
```
|
182
|
+
|
183
|
+
## Testing Multi-Agent Systems
|
184
|
+
|
185
|
+
### Unit Testing Individual Agents
|
186
|
+
|
187
|
+
Test each agent in isolation:
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
RSpec.describe "BillingAgent" do
|
191
|
+
let(:agent) { create_billing_agent }
|
192
|
+
let(:runner) { Agents::AgentRunner.with_agents(agent) }
|
193
|
+
|
194
|
+
it "handles payment inquiries" do
|
195
|
+
result = runner.run("What payment methods do you accept?")
|
196
|
+
expect(result.output).to include("credit card", "bank transfer")
|
197
|
+
end
|
198
|
+
end
|
199
|
+
```
|
200
|
+
|
201
|
+
### Integration Testing Handoffs
|
202
|
+
|
203
|
+
Test complete workflows:
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
RSpec.describe "Customer Support Workflow" do
|
207
|
+
let(:runner) { create_support_runner } # Creates triage + specialists
|
208
|
+
|
209
|
+
it "routes billing questions correctly" do
|
210
|
+
result = runner.run("I have a billing question")
|
211
|
+
|
212
|
+
# Verify handoff occurred
|
213
|
+
expect(result.context.current_agent_name).to eq("Billing")
|
214
|
+
|
215
|
+
# Test continued conversation
|
216
|
+
followup = runner.run("What are your payment terms?", context: result.context)
|
217
|
+
expect(followup.output).to include("payment terms")
|
218
|
+
end
|
219
|
+
end
|
220
|
+
```
|
221
|
+
|
222
|
+
## Common Pitfalls
|
223
|
+
|
224
|
+
### Over-Specialization
|
225
|
+
Don't create too many narrow agents - it increases handoff complexity and latency.
|
226
|
+
|
227
|
+
### Under-Specified Instructions
|
228
|
+
Vague instructions lead to inappropriate handoffs. Be explicit about what each agent should and shouldn't handle.
|
229
|
+
|
230
|
+
### Circular Dependencies
|
231
|
+
Avoid mutual handoffs between agents. Use hub-and-spoke or clear hierarchical patterns instead.
|
232
|
+
|
233
|
+
### Context Leakage
|
234
|
+
Don't rely on shared mutable state. Use the context hash for inter-agent communication.
|
235
|
+
|
236
|
+
## Performance Considerations
|
237
|
+
|
238
|
+
### Handoff Overhead
|
239
|
+
Each handoff adds latency. Design agents to minimize unnecessary transfers.
|
240
|
+
|
241
|
+
### Context Size
|
242
|
+
Large contexts increase token usage. Clean up irrelevant data periodically:
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
# Remove old conversation history
|
246
|
+
cleaned_context = result.context.dup
|
247
|
+
cleaned_context[:conversation_history] = cleaned_context[:conversation_history].last(10)
|
248
|
+
```
|
249
|
+
|
250
|
+
### Concurrent Execution
|
251
|
+
AgentRunner is thread-safe, allowing multiple conversations simultaneously:
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
# Safe to use same runner across threads
|
255
|
+
threads = users.map do |user|
|
256
|
+
Thread.new do
|
257
|
+
user_result = runner.run(user.message, context: user.context)
|
258
|
+
# Handle result...
|
259
|
+
end
|
260
|
+
end
|
261
|
+
```
|
@@ -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
|
+
```
|