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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -106
  3. data/docs/Gemfile +14 -0
  4. data/docs/Gemfile.lock +183 -0
  5. data/docs/_config.yml +53 -0
  6. data/docs/_sass/color_schemes/ruby.scss +72 -0
  7. data/docs/_sass/custom/custom.scss +93 -0
  8. data/docs/architecture.md +353 -0
  9. data/docs/assets/fonts/InterVariable.woff2 +0 -0
  10. data/docs/concepts/agent-tool.md +166 -0
  11. data/docs/concepts/agents.md +43 -0
  12. data/docs/concepts/context.md +110 -0
  13. data/docs/concepts/handoffs.md +81 -0
  14. data/docs/concepts/runner.md +87 -0
  15. data/docs/concepts/tools.md +62 -0
  16. data/docs/concepts.md +21 -0
  17. data/docs/guides/agent-as-tool-pattern.md +242 -0
  18. data/docs/guides/multi-agent-systems.md +261 -0
  19. data/docs/guides/rails-integration.md +440 -0
  20. data/docs/guides/state-persistence.md +451 -0
  21. data/docs/guides.md +18 -0
  22. data/docs/index.md +95 -0
  23. data/examples/collaborative-copilot/README.md +169 -0
  24. data/examples/collaborative-copilot/agents/analysis_agent.rb +48 -0
  25. data/examples/collaborative-copilot/agents/answer_suggestion_agent.rb +50 -0
  26. data/examples/collaborative-copilot/agents/copilot_orchestrator.rb +85 -0
  27. data/examples/collaborative-copilot/agents/integrations_agent.rb +58 -0
  28. data/examples/collaborative-copilot/agents/research_agent.rb +52 -0
  29. data/examples/collaborative-copilot/data/contacts.json +47 -0
  30. data/examples/collaborative-copilot/data/conversations.json +170 -0
  31. data/examples/collaborative-copilot/data/knowledge_base.json +58 -0
  32. data/examples/collaborative-copilot/data/linear_issues.json +83 -0
  33. data/examples/collaborative-copilot/data/stripe_billing.json +71 -0
  34. data/examples/collaborative-copilot/interactive.rb +90 -0
  35. data/examples/collaborative-copilot/tools/create_linear_ticket_tool.rb +58 -0
  36. data/examples/collaborative-copilot/tools/get_article_tool.rb +41 -0
  37. data/examples/collaborative-copilot/tools/get_contact_tool.rb +51 -0
  38. data/examples/collaborative-copilot/tools/get_conversation_tool.rb +53 -0
  39. data/examples/collaborative-copilot/tools/get_stripe_billing_tool.rb +44 -0
  40. data/examples/collaborative-copilot/tools/search_contacts_tool.rb +57 -0
  41. data/examples/collaborative-copilot/tools/search_conversations_tool.rb +54 -0
  42. data/examples/collaborative-copilot/tools/search_knowledge_base_tool.rb +55 -0
  43. data/examples/collaborative-copilot/tools/search_linear_issues_tool.rb +60 -0
  44. data/examples/isp-support/agents_factory.rb +57 -1
  45. data/examples/isp-support/tools/create_lead_tool.rb +16 -2
  46. data/examples/isp-support/tools/crm_lookup_tool.rb +13 -1
  47. data/lib/agents/agent.rb +52 -6
  48. data/lib/agents/agent_tool.rb +113 -0
  49. data/lib/agents/handoff.rb +8 -34
  50. data/lib/agents/tool_context.rb +36 -0
  51. data/lib/agents/version.rb +1 -1
  52. data/lib/agents.rb +1 -0
  53. metadata +44 -2
@@ -0,0 +1,451 @@
1
+ ---
2
+ layout: default
3
+ title: State Persistence
4
+ parent: Guides
5
+ nav_order: 3
6
+ ---
7
+
8
+ # State Persistence
9
+
10
+ The AI Agents library provides flexible mechanisms for persisting state between agent interactions and tool executions. This guide covers context serialization, cross-session persistence, and state management patterns.
11
+
12
+ ## Context Serialization
13
+
14
+ The library's context system is designed to be fully serializable, enabling persistence across process boundaries.
15
+
16
+ ### Basic Serialization
17
+
18
+ Context objects can be converted to and from JSON:
19
+
20
+ ```ruby
21
+ # Run an agent
22
+ result = runner.run("Hello, my name is John")
23
+
24
+ # Serialize context to JSON
25
+ context_json = result.context.to_json
26
+
27
+ # Later, deserialize and continue conversation
28
+ restored_context = JSON.parse(context_json, symbolize_names: true)
29
+ next_result = runner.run("What's my name?", context: restored_context)
30
+ # => "Your name is John"
31
+ ```
32
+
33
+ ### Database Storage
34
+
35
+ Store context in your database for long-term persistence:
36
+
37
+ ```ruby
38
+ # Store context in database
39
+ class ConversationState < ActiveRecord::Base
40
+ def context=(hash)
41
+ self.context_data = hash.to_json
42
+ end
43
+
44
+ def context
45
+ JSON.parse(context_data, symbolize_names: true)
46
+ end
47
+ end
48
+
49
+ # Usage
50
+ conversation = ConversationState.create(
51
+ user_id: user.id,
52
+ context: result.context.to_h
53
+ )
54
+
55
+ # Restore later
56
+ restored_context = conversation.context
57
+ continued_result = runner.run("Continue conversation", context: restored_context)
58
+ ```
59
+
60
+ ## Cross-Session Persistence
61
+
62
+ ### Session-Based State
63
+
64
+ Maintain state across HTTP requests using sessions:
65
+
66
+ ```ruby
67
+ class ChatController < ApplicationController
68
+ def send_message
69
+ # Retrieve context from session
70
+ context = session[:agent_context] || {}
71
+
72
+ # Run agent
73
+ result = agent_runner.run(params[:message], context: context)
74
+
75
+ # Store updated context in session
76
+ session[:agent_context] = result.context.to_h
77
+
78
+ render json: { response: result.output }
79
+ end
80
+
81
+ def reset_conversation
82
+ session[:agent_context] = nil
83
+ render json: { message: "Conversation reset" }
84
+ end
85
+ end
86
+ ```
87
+
88
+ ### File-Based Persistence
89
+
90
+ For development or simple deployments:
91
+
92
+ ```ruby
93
+ class FileContextPersistence
94
+ def initialize(storage_path = "./contexts")
95
+ @storage_path = storage_path
96
+ FileUtils.mkdir_p(@storage_path)
97
+ end
98
+
99
+ def save_context(user_id, context)
100
+ file_path = context_file_path(user_id)
101
+ File.write(file_path, context.to_json)
102
+ end
103
+
104
+ def load_context(user_id)
105
+ file_path = context_file_path(user_id)
106
+ return {} unless File.exist?(file_path)
107
+
108
+ JSON.parse(File.read(file_path), symbolize_names: true)
109
+ end
110
+
111
+ def delete_context(user_id)
112
+ file_path = context_file_path(user_id)
113
+ File.delete(file_path) if File.exist?(file_path)
114
+ end
115
+
116
+ private
117
+
118
+ def context_file_path(user_id)
119
+ File.join(@storage_path, "#{user_id}.json")
120
+ end
121
+ end
122
+
123
+ # Usage
124
+ persistence = FileContextPersistence.new
125
+ context = persistence.load_context(user.id)
126
+
127
+ result = runner.run(message, context: context)
128
+ persistence.save_context(user.id, result.context.to_h)
129
+ ```
130
+
131
+ ## State Management Patterns
132
+
133
+ ### Context Layering
134
+
135
+ Organize context data into logical layers:
136
+
137
+ ```ruby
138
+ def build_layered_context(user, conversation_id)
139
+ {
140
+ # User layer - persistent across all conversations
141
+ user: {
142
+ id: user.id,
143
+ name: user.name,
144
+ preferences: user.preferences
145
+ },
146
+
147
+ # Conversation layer - specific to this conversation
148
+ conversation: {
149
+ id: conversation_id,
150
+ started_at: Time.current,
151
+ topic: nil
152
+ },
153
+
154
+ # Session layer - temporary data for current session
155
+ session: {
156
+ last_activity: Time.current,
157
+ interaction_count: 0
158
+ }
159
+ }
160
+ end
161
+ ```
162
+
163
+ ### Context Cleanup
164
+
165
+ Prevent context from growing indefinitely:
166
+
167
+ ```ruby
168
+ class ContextCleaner
169
+ MAX_HISTORY_SIZE = 20
170
+ MAX_CONTEXT_KEYS = 50
171
+
172
+ def self.clean_context(context)
173
+ cleaned = context.deep_dup
174
+
175
+ # Limit conversation history
176
+ if cleaned[:conversation_history]&.size > MAX_HISTORY_SIZE
177
+ cleaned[:conversation_history] = cleaned[:conversation_history].last(MAX_HISTORY_SIZE)
178
+ end
179
+
180
+ # Remove old temporary data
181
+ cleaned.delete(:temp_data) if cleaned[:temp_data]
182
+
183
+ # Limit total context size
184
+ if cleaned.keys.size > MAX_CONTEXT_KEYS
185
+ # Keep essential keys, remove extras
186
+ essential_keys = [:user_id, :current_agent_name, :conversation_history]
187
+ extra_keys = cleaned.keys - essential_keys
188
+ extra_keys.first(cleaned.keys.size - MAX_CONTEXT_KEYS).each do |key|
189
+ cleaned.delete(key)
190
+ end
191
+ end
192
+
193
+ cleaned
194
+ end
195
+ end
196
+
197
+ # Use in your service
198
+ result = runner.run(message, context: context)
199
+ cleaned_context = ContextCleaner.clean_context(result.context.to_h)
200
+ save_context(user_id, cleaned_context)
201
+ ```
202
+
203
+ ## Tool State Management
204
+
205
+ ### Stateless Tool Design
206
+
207
+ Tools should be stateless and rely on context for all data:
208
+
209
+ ```ruby
210
+ class DatabaseTool < Agents::Tool
211
+ name "query_database"
212
+ description "Query the application database"
213
+ param :query, type: "string", desc: "SQL query to execute"
214
+
215
+ def perform(tool_context, query:)
216
+ # Get database connection from context, not instance variables
217
+ db_config = tool_context.context[:database_config]
218
+ connection = establish_connection(db_config)
219
+
220
+ # Execute query and return results
221
+ connection.execute(query)
222
+ end
223
+
224
+ private
225
+
226
+ def establish_connection(config)
227
+ # Create connection based on config
228
+ ActiveRecord::Base.establish_connection(config)
229
+ end
230
+ end
231
+ ```
232
+
233
+ ### Tool State Persistence
234
+
235
+ Store tool-specific data in context:
236
+
237
+ ```ruby
238
+ class FileProcessorTool < Agents::Tool
239
+ name "process_file"
240
+ description "Process uploaded files"
241
+ param :file_path, type: "string", desc: "Path to file"
242
+
243
+ def perform(tool_context, file_path:)
244
+ # Initialize tool state in context if needed
245
+ tool_context.context[:file_processor] ||= {
246
+ processed_files: [],
247
+ processing_status: {}
248
+ }
249
+
250
+ # Process file
251
+ result = process_file(file_path)
252
+
253
+ # Update tool state in context
254
+ tool_context.context[:file_processor][:processed_files] << file_path
255
+ tool_context.context[:file_processor][:processing_status][file_path] = result[:status]
256
+
257
+ result
258
+ end
259
+ end
260
+ ```
261
+
262
+ ## Advanced Persistence Patterns
263
+
264
+ ### Context Versioning
265
+
266
+ Track context changes over time:
267
+
268
+ ```ruby
269
+ class VersionedContext
270
+ def initialize(initial_context = {})
271
+ @versions = [initial_context.deep_dup]
272
+ @current_version = 0
273
+ end
274
+
275
+ def update_context(new_context)
276
+ @versions << new_context.deep_dup
277
+ @current_version = @versions.size - 1
278
+ end
279
+
280
+ def current_context
281
+ @versions[@current_version]
282
+ end
283
+
284
+ def rollback(versions = 1)
285
+ target_version = [@current_version - versions, 0].max
286
+ @current_version = target_version
287
+ current_context
288
+ end
289
+
290
+ def context_history
291
+ @versions.map.with_index do |context, index|
292
+ {
293
+ version: index,
294
+ timestamp: context[:updated_at],
295
+ agent: context[:current_agent_name]
296
+ }
297
+ end
298
+ end
299
+ end
300
+ ```
301
+
302
+ ### Context Encryption
303
+
304
+ Encrypt sensitive context data:
305
+
306
+ ```ruby
307
+ class EncryptedContextStorage
308
+ def initialize(encryption_key)
309
+ @cipher = OpenSSL::Cipher.new('AES-256-CBC')
310
+ @key = encryption_key
311
+ end
312
+
313
+ def encrypt_context(context)
314
+ @cipher.encrypt
315
+ @cipher.key = @key
316
+
317
+ encrypted_data = @cipher.update(context.to_json)
318
+ encrypted_data << @cipher.final
319
+
320
+ Base64.encode64(encrypted_data)
321
+ end
322
+
323
+ def decrypt_context(encrypted_data)
324
+ @cipher.decrypt
325
+ @cipher.key = @key
326
+
327
+ decoded_data = Base64.decode64(encrypted_data)
328
+ decrypted_data = @cipher.update(decoded_data)
329
+ decrypted_data << @cipher.final
330
+
331
+ JSON.parse(decrypted_data, symbolize_names: true)
332
+ end
333
+ end
334
+
335
+ # Usage
336
+ storage = EncryptedContextStorage.new(Rails.application.secret_key_base)
337
+
338
+ # Encrypt before storing
339
+ encrypted_context = storage.encrypt_context(result.context.to_h)
340
+ database_record.update(encrypted_context: encrypted_context)
341
+
342
+ # Decrypt when loading
343
+ encrypted_data = database_record.encrypted_context
344
+ context = storage.decrypt_context(encrypted_data)
345
+ ```
346
+
347
+ ### Distributed Context Storage
348
+
349
+ For multi-server deployments using Redis:
350
+
351
+ ```ruby
352
+ class RedisContextStorage
353
+ def initialize(redis_client = Redis.new)
354
+ @redis = redis_client
355
+ end
356
+
357
+ def save_context(user_id, context, ttl: 1.hour)
358
+ key = context_key(user_id)
359
+ @redis.setex(key, ttl, context.to_json)
360
+ end
361
+
362
+ def load_context(user_id)
363
+ key = context_key(user_id)
364
+ data = @redis.get(key)
365
+ return {} unless data
366
+
367
+ JSON.parse(data, symbolize_names: true)
368
+ end
369
+
370
+ def delete_context(user_id)
371
+ key = context_key(user_id)
372
+ @redis.del(key)
373
+ end
374
+
375
+ def extend_ttl(user_id, ttl: 1.hour)
376
+ key = context_key(user_id)
377
+ @redis.expire(key, ttl)
378
+ end
379
+
380
+ private
381
+
382
+ def context_key(user_id)
383
+ "agent_context:#{user_id}"
384
+ end
385
+ end
386
+ ```
387
+
388
+ ## Context Migration
389
+
390
+ Handle context format changes across application versions:
391
+
392
+ ```ruby
393
+ class ContextMigrator
394
+ CURRENT_VERSION = 2
395
+
396
+ def self.migrate_context(context)
397
+ version = context[:_version] || 1
398
+
399
+ case version
400
+ when 1
401
+ migrate_v1_to_v2(context)
402
+ when 2
403
+ context # Already current
404
+ else
405
+ raise "Unknown context version: #{version}"
406
+ end
407
+ end
408
+
409
+ private
410
+
411
+ def self.migrate_v1_to_v2(context)
412
+ # V1 -> V2: Rename 'current_agent' to 'current_agent_name'
413
+ migrated = context.deep_dup
414
+
415
+ if migrated[:current_agent]
416
+ migrated[:current_agent_name] = migrated.delete(:current_agent)
417
+ end
418
+
419
+ migrated[:_version] = 2
420
+ migrated
421
+ end
422
+ end
423
+
424
+ # Use in context loading
425
+ def load_context(user_id)
426
+ raw_context = storage.load_context(user_id)
427
+ ContextMigrator.migrate_context(raw_context)
428
+ end
429
+ ```
430
+
431
+ ## Best Practices
432
+
433
+ ### Context Size Management
434
+ - Regularly clean up old conversation history
435
+ - Remove temporary data after use
436
+ - Set reasonable size limits for context values
437
+
438
+ ### Security Considerations
439
+ - Encrypt sensitive data in persistent storage
440
+ - Validate context data when loading from external sources
441
+ - Sanitize context data before serialization
442
+
443
+ ### Performance Optimization
444
+ - Use lazy loading for large context objects
445
+ - Cache frequently accessed context data
446
+ - Consider context compression for large datasets
447
+
448
+ ### Error Handling
449
+ - Always validate context structure after deserialization
450
+ - Provide fallback default contexts for corrupted data
451
+ - Log context-related errors for debugging
data/docs/guides.md ADDED
@@ -0,0 +1,18 @@
1
+ ---
2
+ layout: default
3
+ title: Guides
4
+ nav_order: 3
5
+ published: false
6
+ has_children: true
7
+ ---
8
+
9
+ # Guides
10
+
11
+ Practical guides for building real-world applications with the AI Agents library. These guides provide step-by-step instructions, code examples, and best practices for common use cases.
12
+
13
+ ## Available Guides
14
+
15
+ - **[Building Multi-Agent Systems](guides/multi-agent-systems.html)** - Design patterns and best practices for creating collaborative agent workflows
16
+ - **[Agent-as-Tool Pattern](guides/agent-as-tool-pattern.html)** - Enable agent collaboration behind the scenes without conversation handoffs
17
+ - **[Rails Integration](guides/rails-integration.html)** - Integrating agents with Ruby on Rails applications and ActiveRecord persistence
18
+ - **[State Persistence](guides/state-persistence.html)** - Managing conversation state and context across sessions and processes
data/docs/index.md ADDED
@@ -0,0 +1,95 @@
1
+ ---
2
+ layout: default
3
+ title: Home
4
+ nav_order: 1
5
+ description: "AI Agents is a Ruby SDK for building multi-agent AI workflows."
6
+ permalink: /
7
+ ---
8
+
9
+ # AI Agents
10
+
11
+ A Ruby SDK for building sophisticated multi-agent AI workflows.
12
+
13
+ {: .fs-6 .fw-300 }
14
+
15
+ [Get started now](#getting-started){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 }
16
+ [View it on GitHub](https://github.com/chatwoot/ai-agents){: .btn .fs-5 .mb-4 .mb-md-0 }
17
+
18
+ ---
19
+
20
+ ## What is AI Agents?
21
+
22
+ AI Agents is a Ruby SDK that enables developers to create sophisticated multi-agent AI workflows. Build specialized AI agents that can collaborate, use tools, and seamlessly hand off conversations to solve complex tasks.
23
+
24
+ ### Key Features
25
+
26
+ - **Multi-Agent Orchestration**: Define and manage multiple AI agents with distinct roles
27
+ - **Seamless Handoffs**: Transfer conversations between agents without user knowledge
28
+ - **Tool Integration**: Allow agents to use custom tools to interact with external systems
29
+ - **Shared Context**: Maintain state and conversation history across agent interactions
30
+ - **Thread-Safe Architecture**: Reusable agent runners that work safely across multiple threads
31
+ - **Provider Agnostic**: Support for OpenAI, Anthropic, and Gemini
32
+
33
+ ## Getting Started
34
+
35
+ ### Installation
36
+
37
+ Add this line to your application's Gemfile:
38
+
39
+ ```ruby
40
+ gem 'ai-agents'
41
+ ```
42
+
43
+ And then execute:
44
+
45
+ ```bash
46
+ bundle install
47
+ ```
48
+
49
+ Or install it yourself as:
50
+
51
+ ```bash
52
+ gem install ai-agents
53
+ ```
54
+
55
+ ### Quick Start
56
+
57
+ ```ruby
58
+ require 'agents'
59
+
60
+ # Configure your API keys
61
+ Agents.configure do |config|
62
+ config.openai_api_key = ENV['OPENAI_API_KEY']
63
+ # config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
64
+ # config.gemini_api_key = ENV['GEMINI_API_KEY']
65
+ config.default_model = 'gpt-4o-mini'
66
+ end
67
+
68
+ # Create agents
69
+ triage = Agents::Agent.new(
70
+ name: "Triage",
71
+ instructions: "You help route customer inquiries to the right department."
72
+ )
73
+
74
+ support = Agents::Agent.new(
75
+ name: "Support",
76
+ instructions: "You provide technical support for our products."
77
+ )
78
+
79
+ # Set up handoffs
80
+ triage.register_handoffs(support)
81
+
82
+ # Create runner and start conversation
83
+ runner = Agents::AgentRunner.with_agents(triage, support)
84
+ result = runner.run("I need help with a technical issue")
85
+
86
+ puts result.output
87
+ ```
88
+
89
+ ## Next Steps
90
+
91
+ - [Learn about Agents](concepts/agents.html)
92
+ - [Understand Context](concepts/context.html)
93
+ - [Working with Tools](concepts/tools.html)
94
+ - [Agent Handoffs](concepts/handoffs.html)
95
+ - [Using the Runner](concepts/runner.html)