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,81 @@
1
+ ---
2
+ layout: default
3
+ title: Handoffs
4
+ parent: Concepts
5
+ nav_order: 4
6
+ ---
7
+
8
+ # Handoffs
9
+
10
+ **Handoffs** are a powerful feature of the Ruby Agents library that allow you to build sophisticated multi-agent systems. A handoff is the process of transferring a conversation from one agent to another, more specialized agent.
11
+
12
+ This is particularly useful when you have a general-purpose agent that can handle a wide range of queries, but you also have specialized agents that are better equipped to handle specific tasks. For example, you might have a triage agent that routes users to a billing agent or a technical support agent.
13
+
14
+ ## How Handoffs Work
15
+
16
+ Handoffs are implemented as a special type of tool called a `HandoffTool`. When you configure an agent with `handoff_agents`, the library automatically creates a `HandoffTool` for each of the specified agents.
17
+
18
+ Here's how the handoff process works:
19
+
20
+ 1. **The user sends a message:** The user sends a message that indicates they need help with a specific task (e.g., "I have a problem with my bill").
21
+ 2. **The LLM decides to hand off:** The current agent's language model determines that the query is best handled by another agent and decides to call the corresponding `HandoffTool`.
22
+ 3. **The `HandoffTool` signals the handoff:** The `HandoffTool` sets a `pending_handoff` flag in the `RunContext`, indicating which agent to hand off to.
23
+ 4. **The Runner switches agents:** The `Runner` detects the `pending_handoff` flag and switches the `current_agent` to the new agent.
24
+ 5. **The conversation continues:** The conversation continues with the new agent, which now has access to the full conversation history.
25
+
26
+ ### Loop Prevention
27
+
28
+ To prevent infinite handoff loops, the library automatically processes only the first handoff tool call in any LLM response. If multiple handoff tools are called in a single response, only the first one is executed and subsequent calls are ignored. This prevents conflicting handoff states and ensures clean agent transitions.
29
+
30
+ ## Why Use Tools for Handoffs?
31
+
32
+ Using tools for handoffs has several advantages over simply instructing the LLM to hand off the conversation:
33
+
34
+ * **Reliability:** LLMs are very good at using tools when they are available. By representing handoffs as tools, we can be more confident that the LLM will use them when appropriate.
35
+ * **Clarity:** The tool's schema clearly defines when each handoff is suitable, making it easier for the LLM to make the right decision.
36
+ * **Simplicity:** We don't need to parse free-text responses from the LLM to determine if a handoff is needed.
37
+ * **Consistency:** This approach works consistently across different LLM providers.
38
+
39
+ ## Example
40
+
41
+ ```ruby
42
+ # Create the specialized agents
43
+ billing_agent = Agents::Agent.new(name: "Billing", instructions: "Handle billing and payment issues.")
44
+ support_agent = Agents::Agent.new(name: "Support", instructions: "Provide technical support.")
45
+
46
+ # Create the triage agent with handoff agents
47
+ triage_agent = Agents::Agent.new(
48
+ name: "Triage",
49
+ instructions: "You are a triage agent. Your job is to route users to the correct department.",
50
+ handoff_agents: [billing_agent, support_agent]
51
+ )
52
+
53
+ # Run the triage agent
54
+ result = Agents::Runner.run(triage_agent, "I have a problem with my bill.")
55
+
56
+ # The runner will automatically hand off to the billing agent
57
+ ```
58
+
59
+ In this example, the `triage_agent` will automatically hand off the conversation to the `billing_agent` when the user asks a question about their bill. This allows you to create a seamless user experience where the user is always talking to the most qualified agent for their needs.
60
+
61
+ ## Troubleshooting Handoffs
62
+
63
+ ### Infinite Handoff Loops
64
+
65
+ **Problem:** Agents keep handing off to each other in an endless loop.
66
+
67
+ **Common Causes:**
68
+ - Agent instructions that conflict with each other
69
+ - Agents configured to hand off for overlapping scenarios
70
+ - Poor instruction clarity about when to hand off vs. when to handle directly
71
+
72
+ **Solutions:**
73
+ 1. **Review agent instructions:** Ensure each agent has a clear, distinct responsibility
74
+ 2. **Use hub-and-spoke pattern:** Have specialized agents only hand off back to a central triage agent
75
+ 3. **Add specific scenarios:** Include examples in instructions of when to handle vs. hand off
76
+ 4. **Enable debug logging:** Use `ENV["RUBYLLM_DEBUG"] = "true"` to see handoff decisions
77
+
78
+
79
+ ### Multiple Handoffs in One Response
80
+
81
+ The library automatically handles cases where an LLM tries to call multiple handoff tools in a single response. Only the first handoff will be processed, and subsequent calls will be ignored. This is normal behavior and prevents conflicting handoff states.
@@ -0,0 +1,87 @@
1
+ ---
2
+ layout: default
3
+ title: Runner
4
+ parent: Concepts
5
+ nav_order: 5
6
+ ---
7
+
8
+ # AgentRunner
9
+
10
+ The **AgentRunner** is the thread-safe execution manager that provides the main API for multi-agent conversations. It separates agent registry management from execution, enabling safe concurrent use across multiple threads while maintaining conversation continuity.
11
+
12
+ ## Two-Tier Architecture
13
+
14
+ The library uses a two-tier design separating long-lived configuration from short-lived execution:
15
+
16
+ ### AgentRunner (Thread-Safe Manager)
17
+ - Created once at application startup
18
+ - Maintains immutable agent registry
19
+ - Determines conversation continuity from history
20
+ - Thread-safe for concurrent conversations
21
+
22
+ ### Runner (Internal Execution Engine)
23
+ - Created per conversation turn
24
+ - Handles LLM communication and tool execution
25
+ - Manages context state during execution
26
+ - Stateless and garbage-collected after use
27
+
28
+ ## Conversation Flow
29
+
30
+ Each conversation follows this flow:
31
+
32
+ 1. **Agent Selection**: AgentRunner determines current agent from conversation history
33
+ 2. **Context Isolation**: Creates deep copy of context for thread safety
34
+ 3. **LLM Communication**: Sends message with context to language model
35
+ 4. **Tool Execution**: Executes any requested tools through RubyLLM
36
+ 5. **Handoff Detection**: Checks for agent handoffs and switches if needed
37
+ 6. **State Persistence**: Updates context with conversation state
38
+
39
+ ## Thread Safety
40
+
41
+ The AgentRunner ensures thread safety through several key mechanisms:
42
+
43
+ * **Immutable Registry**: Agent registry is frozen after initialization, preventing runtime modifications
44
+ * **Context Isolation**: Each execution receives a deep copy of context to prevent cross-contamination
45
+ * **Stateless Execution**: Internal Runner instances store no execution-specific state
46
+ * **Tool Wrapping**: ToolWrapper injects context through parameters, keeping tools stateless
47
+
48
+ ## Usage Pattern
49
+
50
+ Create an AgentRunner once and reuse it for multiple conversations:
51
+
52
+ ```ruby
53
+ # Create agents
54
+ triage_agent = Agents::Agent.new(
55
+ name: "Triage",
56
+ instructions: "Route users to appropriate specialists"
57
+ )
58
+ billing_agent = Agents::Agent.new(
59
+ name: "Billing",
60
+ instructions: "Handle billing inquiries"
61
+ )
62
+
63
+ # Register handoffs
64
+ triage_agent.register_handoffs(billing_agent)
65
+
66
+ # Create runner once (thread-safe)
67
+ runner = Agents::Runner.with_agents(triage_agent, billing_agent)
68
+
69
+ # Use from multiple threads safely
70
+ result1 = runner.run("I have a billing question")
71
+ result2 = runner.run("Follow up", context: result1.context)
72
+ ```
73
+
74
+ ## Conversation Continuity
75
+
76
+ The AgentRunner automatically maintains conversation continuity by analyzing message history to determine which agent should handle each turn:
77
+
78
+ ```ruby
79
+ # First message -> Uses triage agent (default entry point)
80
+ result1 = runner.run("I need help with my bill")
81
+
82
+ # Triage hands off to billing agent
83
+ # Next message -> AgentRunner detects billing agent should continue
84
+ result2 = runner.run("What payment methods do you accept?", context: result1.context)
85
+ ```
86
+
87
+ The `run` method returns a `RunResult` with output, conversation history, usage metrics, and updated context.
@@ -0,0 +1,62 @@
1
+ ---
2
+ layout: default
3
+ title: Tools
4
+ parent: Concepts
5
+ nav_order: 2
6
+ ---
7
+
8
+ # Tools
9
+
10
+ **Tools** are the components that allow agents to interact with the outside world. They are the primary way to extend an agent's capabilities beyond what the language model can do on its own. A tool can be anything from a simple calculator to a complex integration with an external API.
11
+
12
+ In Ruby Agents, tools are designed to be thread-safe and stateless. This is a critical design principle that ensures the stability and reliability of your agent system, especially in concurrent environments.
13
+
14
+ ## Thread-Safe Design
15
+
16
+ The key to the thread-safe design of tools is that they do not store any execution-specific state in their instance variables. All the data a tool needs to perform its action is passed to it through the `perform` method, which receives a `ToolContext` object.
17
+
18
+ ### The `ToolContext`
19
+
20
+ The `ToolContext` provides access to the current execution context, including:
21
+
22
+ * **Shared context data:** A hash of data that is shared across all tools and agents in a given run.
23
+ * **Usage tracking:** An object that tracks token usage for the current run.
24
+ * **Retry count:** The number of times the current tool execution has been retried.
25
+
26
+ By passing all the necessary data through the `ToolContext`, we ensure that tool instances can be safely shared across multiple threads without the risk of data corruption.
27
+
28
+ ## Creating a Tool
29
+
30
+ You can create a tool in two ways:
31
+
32
+ 1. **Creating a Tool Class:** For more complex tools, you can create a class that inherits from `Agents::Tool` and implements the `perform` method.
33
+
34
+ ```ruby
35
+ class WeatherTool < Agents::Tool
36
+ name "get_weather"
37
+ description "Get the current weather for a location."
38
+ param :location, type: "string", desc: "The city and state, e.g., San Francisco, CA"
39
+
40
+ def perform(tool_context, location:)
41
+ # Access the API key from the shared context
42
+ api_key = tool_context.context[:weather_api_key]
43
+
44
+ # Call the weather API and return the result
45
+ WeatherApi.get(location, api_key)
46
+ end
47
+ end
48
+ ```
49
+
50
+ 2. **Using the Functional Tool Definition:** For simpler tools, you can use the `Agents::Tool.tool` helper to define a tool with a block.
51
+
52
+ ```ruby
53
+ calculator_tool = Agents::Tool.tool(
54
+ "calculate",
55
+ description: "Perform a mathematical calculation."
56
+ ) do |tool_context, expression:|
57
+ # Perform the calculation and return the result
58
+ eval(expression).to_s
59
+ end
60
+ ```
61
+
62
+ In both cases, the `perform` method receives the `tool_context` and the tool's parameters as arguments. This design ensures that your tools are always thread-safe and easy to test.
data/docs/concepts.md ADDED
@@ -0,0 +1,21 @@
1
+ ---
2
+ layout: default
3
+ title: Concepts
4
+ nav_order: 2
5
+ has_children: true
6
+ ---
7
+
8
+ # Concepts
9
+
10
+ This section covers the core concepts of the AI Agents library. Understanding these concepts is essential for building robust and scalable AI agent systems.
11
+
12
+ ## Overview
13
+
14
+ The AI Agents library is built around several key concepts that work together to provide a powerful framework for multi-agent AI workflows:
15
+
16
+ - **[Agents](concepts/agents.html)** - Immutable, thread-safe AI assistants with specific roles and capabilities
17
+ - **[AgentRunner](concepts/runner.html)** - Thread-safe execution manager for multi-agent conversations
18
+ - **[Context](concepts/context.html)** - Serializable state management that persists across agent interactions
19
+ - **[Handoffs](concepts/handoffs.html)** - Tool-based mechanism for seamless agent transitions
20
+ - **[Tools](concepts/tools.html)** - Stateless extensions for external system integration
21
+ - **[AgentTool](concepts/agent-tool.html)** - Agent-to-agent collaboration without conversation handoffs
@@ -0,0 +1,242 @@
1
+ ---
2
+ layout: default
3
+ title: Agent-as-Tool Pattern
4
+ parent: Guides
5
+ nav_order: 4
6
+ ---
7
+
8
+ # Agent-as-Tool Pattern Guide
9
+
10
+ The Agent-as-Tool pattern enables **multi-agent collaboration** where specialized agents work behind the scenes to help each other, without the user knowing multiple agents are involved.
11
+
12
+ ## When to Use This Pattern
13
+
14
+ Use Agent-as-Tool when you need:
15
+
16
+ - **Specialized Processing**: Different agents excel at different tasks
17
+ - **Behind-the-Scenes Coordination**: Agents collaborate invisibly to the user
18
+ - **Multi-step Workflows**: Complex processes requiring different expertise
19
+ - **Modular Architecture**: Clean separation of concerns between agents
20
+
21
+ ## Core Concept
22
+
23
+ ### Handoffs vs Agent-as-Tool
24
+
25
+ **Handoffs**: "Let me transfer you to billing"
26
+ - User-visible conversation transfer
27
+ - Full context shared
28
+ - Agent takes over conversation
29
+
30
+ **Agent-as-Tool**: "Let me check that for you" (uses billing agent internally)
31
+ - Invisible to user
32
+ - Limited context (state only)
33
+ - Returns control to caller
34
+
35
+ ## Basic Implementation
36
+
37
+ ### Step 1: Create Specialized Agents
38
+
39
+ ```ruby
40
+ # Research agent - finds customer information
41
+ research_agent = Agents::Agent.new(
42
+ name: "ResearchAgent",
43
+ instructions: <<~PROMPT
44
+ You research customer information and history.
45
+ Return contact details including email addresses.
46
+ PROMPT,
47
+ tools: [customer_lookup_tool, conversation_search_tool]
48
+ )
49
+
50
+ # Billing agent - handles payment operations
51
+ billing_agent = Agents::Agent.new(
52
+ name: "BillingAgent",
53
+ instructions: <<~PROMPT
54
+ You handle billing operations using Stripe.
55
+ CRITICAL: You need customer email addresses for billing lookups.
56
+ Contact IDs will NOT work.
57
+ PROMPT,
58
+ tools: [stripe_billing_tool]
59
+ )
60
+ ```
61
+
62
+ ### Step 2: Create Orchestrator with Agent Tools
63
+
64
+ ```ruby
65
+ # Main agent coordinates specialists
66
+ orchestrator = Agents::Agent.new(
67
+ name: "SupportCopilot",
68
+ instructions: <<~PROMPT
69
+ You help support agents by coordinating specialist agents.
70
+
71
+ **CRITICAL: Multi-Step Workflow Approach**
72
+
73
+ For complex queries, break them into steps and use tools sequentially:
74
+ 1. Plan your approach: What information do you need?
75
+ 2. Execute sequentially: Use EXACT results from previous tools
76
+ 3. Build context progressively: Each tool builds on previous findings
77
+
78
+ **Tool Requirements:**
79
+ - research_customer: Returns contact details including emails
80
+ - check_billing: Requires customer email (not contact ID)
81
+
82
+ Always think: "What did I learn and how do I use it next?"
83
+ PROMPT,
84
+ tools: [
85
+ research_agent.as_tool(
86
+ name: "research_customer",
87
+ description: "Research customer details. Returns contact info including email."
88
+ ),
89
+ billing_agent.as_tool(
90
+ name: "check_billing",
91
+ description: "Check billing status. Requires customer email address."
92
+ )
93
+ ]
94
+ )
95
+ ```
96
+
97
+ ### Step 3: Use with Context Persistence
98
+
99
+ ```ruby
100
+ # Set up runner with context persistence
101
+ runner = Agents::Runner.with_agents(orchestrator)
102
+ context = {}
103
+
104
+ # Interactive loop maintains context
105
+ loop do
106
+ user_input = gets.chomp
107
+ break if user_input == "exit"
108
+
109
+ # Pass and update context each turn
110
+ result = runner.run(user_input, context: context)
111
+ context = result.context if result.context
112
+
113
+ puts result.output
114
+ end
115
+ ```
116
+
117
+ ## Advanced Features
118
+
119
+ ### Custom Output Extraction
120
+
121
+ Extract specific information for other tools:
122
+
123
+ ```ruby
124
+ research_agent.as_tool(
125
+ name: "get_customer_email",
126
+ description: "Get customer email address",
127
+ output_extractor: ->(result) {
128
+ # Extract just the email instead of full response
129
+ email_match = result.output.match(/Email:\s*([^\s]+)/i)
130
+ email_match&.captures&.first || "Email not found"
131
+ }
132
+ )
133
+ ```
134
+
135
+ ## Best Practices
136
+
137
+ ### 1. Clear Tool Descriptions with Requirements
138
+
139
+ Specify what each tool needs and provides:
140
+
141
+ ```ruby
142
+ # Good: Clear requirements
143
+ billing_agent.as_tool(
144
+ name: "check_stripe_billing",
145
+ description: "Check Stripe billing info. Requires customer email (not contact ID)."
146
+ )
147
+
148
+ research_agent.as_tool(
149
+ name: "research_customer",
150
+ description: "Research customer details. Returns email address and contact info."
151
+ )
152
+
153
+ # Avoid: Vague descriptions
154
+ agent.as_tool(name: "process", description: "Do stuff")
155
+ ```
156
+
157
+ ### 2. Multi-Step Workflow Instructions
158
+
159
+ Guide orchestrators to chain tool calls properly:
160
+
161
+ ```ruby
162
+ orchestrator = Agent.new(
163
+ instructions: <<~PROMPT
164
+ **For complex queries requiring multiple pieces of information:**
165
+
166
+ 1. Plan what information you need to gather
167
+ 2. Use tools sequentially, building on previous results
168
+ 3. Extract specific values from tool outputs for subsequent calls
169
+ 4. Don't pass original parameters - use discovered values
170
+
171
+ **Example:** To check billing for CONTACT-123:
172
+ Step 1: research_customer("Get details for CONTACT-123") → finds email
173
+ Step 2: check_billing("Check billing for [discovered email]") → not original ID
174
+ PROMPT
175
+ )
176
+ ```
177
+
178
+ ### 3. Explicit Parameter Requirements in Agent Instructions
179
+
180
+ Make tool parameter needs crystal clear:
181
+
182
+ ```ruby
183
+ billing_agent = Agent.new(
184
+ instructions: <<~PROMPT
185
+ **CRITICAL: Billing Requirements**
186
+ - Stripe billing lookups REQUIRE customer email addresses
187
+ - Contact IDs, names, phone numbers will NOT work
188
+ - If you don't have email, clearly state you need it
189
+ PROMPT
190
+ )
191
+ ```
192
+
193
+ ### 4. Handle Errors with Guidance
194
+
195
+ Provide helpful error messages that guide next steps:
196
+
197
+ ```ruby
198
+ # In orchestrator instructions
199
+ instructions = <<~PROMPT
200
+ **Error Handling:**
201
+ - If billing fails due to missing email: Use research_customer first
202
+ - If contact not found: Ask for more identifying information
203
+ - Always provide helpful responses even if tools fail
204
+ PROMPT
205
+ ```
206
+
207
+ ### 5. Context Persistence for Multi-Turn Conversations
208
+
209
+ Maintain state across conversation turns:
210
+
211
+ ```ruby
212
+ # Maintain context between interactions
213
+ runner = Agents::Runner.with_agents(orchestrator)
214
+ context = {}
215
+
216
+ # Each turn builds on previous context
217
+ result = runner.run(user_input, context: context)
218
+ context = result.context if result.context
219
+ ```
220
+
221
+ ### 6. Design Focused Agents
222
+
223
+ Keep agent responsibilities clear and narrow:
224
+
225
+ ```ruby
226
+ # Good: Focused responsibility
227
+ customer_agent = Agent.new(
228
+ name: "CustomerAgent",
229
+ instructions: "Handle customer data lookups and history research"
230
+ )
231
+
232
+ # Avoid: Too broad
233
+ everything_agent = Agent.new(
234
+ name: "EverythingAgent",
235
+ instructions: "Handle all customer operations, billing, support, and analysis"
236
+ )
237
+ ```
238
+
239
+ ## See Also
240
+
241
+ - [AgentTool Concept](../concepts/agent-tool.html)
242
+ - [Multi-Agent Systems Guide](multi-agent-systems.html)