ai-agents 0.1.2 → 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/lib/agents/agent.rb +34 -0
- data/lib/agents/agent_tool.rb +113 -0
- data/lib/agents/handoff.rb +8 -34
- data/lib/agents/version.rb +1 -1
- data/lib/agents.rb +1 -0
- metadata +43 -1
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Copilot
|
6
|
+
# Tool for searching knowledge base articles and documentation
|
7
|
+
class SearchKnowledgeBaseTool < Agents::Tool
|
8
|
+
description "Search help documentation and knowledge base for solutions"
|
9
|
+
param :query, type: "string", desc: "Search terms or keywords to find relevant articles"
|
10
|
+
param :category, type: "string",
|
11
|
+
desc: "Optional: filter by category (troubleshooting, account, development, billing)", required: false
|
12
|
+
|
13
|
+
def perform(_tool_context, query:, category: nil)
|
14
|
+
data_file = File.join(__dir__, "../data/knowledge_base.json")
|
15
|
+
return "Knowledge base unavailable" unless File.exist?(data_file)
|
16
|
+
|
17
|
+
begin
|
18
|
+
kb_data = JSON.parse(File.read(data_file))
|
19
|
+
articles = kb_data["articles"]
|
20
|
+
query_text = query.downcase
|
21
|
+
matches = []
|
22
|
+
|
23
|
+
articles.each do |article_id, article|
|
24
|
+
# Skip if category filter is specified and doesn't match
|
25
|
+
next if category && article["category"] != category.downcase
|
26
|
+
|
27
|
+
# Simple keyword matching
|
28
|
+
text_to_search = [
|
29
|
+
article["title"],
|
30
|
+
article["tags"].join(" "),
|
31
|
+
article["content"]
|
32
|
+
].join(" ").downcase
|
33
|
+
|
34
|
+
next unless text_to_search.include?(query_text)
|
35
|
+
|
36
|
+
matches << {
|
37
|
+
article_id: article_id,
|
38
|
+
title: article["title"],
|
39
|
+
category: article["category"],
|
40
|
+
tags: article["tags"],
|
41
|
+
content_preview: "#{article["content"][0...200]}..."
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
if matches.empty?
|
46
|
+
"No knowledge base articles found for: #{query}"
|
47
|
+
else
|
48
|
+
JSON.pretty_generate({ query: query, articles: matches })
|
49
|
+
end
|
50
|
+
rescue StandardError => e
|
51
|
+
"Error searching knowledge base: #{e.message}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Copilot
|
6
|
+
# Tool for searching Linear issues for development context and bug reports
|
7
|
+
class SearchLinearIssuesTool < Agents::Tool
|
8
|
+
description "Search Linear issues for bug reports, feature requests, and development context"
|
9
|
+
param :query, type: "string", desc: "Search terms (keywords, error messages, or feature descriptions)"
|
10
|
+
param :status, type: "string", desc: "Optional: filter by status (backlog, in_progress, completed, resolved)",
|
11
|
+
required: false
|
12
|
+
param :priority, type: "string", desc: "Optional: filter by priority (low, medium, high)", required: false
|
13
|
+
|
14
|
+
def perform(_tool_context, query:, status: nil, priority: nil)
|
15
|
+
data_file = File.join(__dir__, "../data/linear_issues.json")
|
16
|
+
return "Linear issues database unavailable" unless File.exist__(data_file)
|
17
|
+
|
18
|
+
begin
|
19
|
+
linear_data = JSON.parse(File.read(data_file))
|
20
|
+
issues = linear_data["issues"]
|
21
|
+
query_text = query.downcase
|
22
|
+
matches = []
|
23
|
+
|
24
|
+
issues.each do |issue_id, issue|
|
25
|
+
# Apply filters
|
26
|
+
next if status && issue["status"] != status.downcase
|
27
|
+
next if priority && issue["priority"] != priority.downcase
|
28
|
+
|
29
|
+
# Simple keyword matching
|
30
|
+
text_to_search = [
|
31
|
+
issue["title"],
|
32
|
+
issue["description"],
|
33
|
+
issue["labels"].join(" "),
|
34
|
+
issue["resolution"]
|
35
|
+
].compact.join(" ").downcase
|
36
|
+
|
37
|
+
next unless text_to_search.include?(query_text)
|
38
|
+
|
39
|
+
matches << {
|
40
|
+
issue_id: issue_id,
|
41
|
+
title: issue["title"],
|
42
|
+
status: issue["status"],
|
43
|
+
priority: issue["priority"],
|
44
|
+
labels: issue["labels"],
|
45
|
+
description: "#{issue["description"][0...200]}...",
|
46
|
+
resolution: issue["resolution"]
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
if matches.empty?
|
51
|
+
"No Linear issues found for: #{query}"
|
52
|
+
else
|
53
|
+
JSON.pretty_generate({ query: query, issues: matches })
|
54
|
+
end
|
55
|
+
rescue StandardError => e
|
56
|
+
"Error searching Linear issues: #{e.message}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/agents/agent.rb
CHANGED
@@ -178,5 +178,39 @@ module Agents
|
|
178
178
|
instructions.call(context)
|
179
179
|
end
|
180
180
|
end
|
181
|
+
|
182
|
+
# Transform this agent into a tool, callable by other agents.
|
183
|
+
# This enables agent-to-agent collaboration without conversation handoffs.
|
184
|
+
#
|
185
|
+
# Agent-as-tool is different from handoffs in two key ways:
|
186
|
+
# 1. The wrapped agent receives generated input, not conversation history
|
187
|
+
# 2. The wrapped agent returns a result to the calling agent, rather than taking over
|
188
|
+
#
|
189
|
+
# @param name [String, nil] Override the tool name (defaults to snake_case agent name)
|
190
|
+
# @param description [String, nil] Override the tool description
|
191
|
+
# @param output_extractor [Proc, nil] Custom proc to extract/transform the agent's output
|
192
|
+
# @param params [Hash] Additional parameter definitions for the tool
|
193
|
+
# @return [Agents::AgentTool] A tool that wraps this agent
|
194
|
+
#
|
195
|
+
# @example Basic agent-as-tool
|
196
|
+
# research_agent = Agent.new(name: "Researcher", instructions: "Research topics")
|
197
|
+
# research_tool = research_agent.as_tool(
|
198
|
+
# name: "research_topic",
|
199
|
+
# description: "Research a topic using company knowledge base"
|
200
|
+
# )
|
201
|
+
#
|
202
|
+
# @example Custom output extraction
|
203
|
+
# analyzer_tool = analyzer_agent.as_tool(
|
204
|
+
# output_extractor: ->(result) { result.context[:extracted_data]&.to_json || result.output }
|
205
|
+
# )
|
206
|
+
#
|
207
|
+
def as_tool(name: nil, description: nil, output_extractor: nil)
|
208
|
+
AgentTool.new(
|
209
|
+
agent: self,
|
210
|
+
name: name,
|
211
|
+
description: description,
|
212
|
+
output_extractor: output_extractor
|
213
|
+
)
|
214
|
+
end
|
181
215
|
end
|
182
216
|
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Agents
|
4
|
+
# AgentTool wraps an agent as a tool, enabling agent-to-agent collaboration
|
5
|
+
# without conversation handoffs. This implementation constrains wrapped agents
|
6
|
+
# for safety and predictability.
|
7
|
+
#
|
8
|
+
# Key constraints:
|
9
|
+
# 1. Wrapped agents cannot perform handoffs (empty registry)
|
10
|
+
# 2. Limited turn count to prevent infinite loops
|
11
|
+
# 3. Isolated context (only shared state, no conversation history)
|
12
|
+
# 4. Always returns to calling agent
|
13
|
+
#
|
14
|
+
# @example Customer support copilot with specialized agents
|
15
|
+
# conversation_agent = Agent.new(
|
16
|
+
# name: "ConversationAnalyzer",
|
17
|
+
# instructions: "Extract order IDs and customer intent from conversation history"
|
18
|
+
# )
|
19
|
+
#
|
20
|
+
# actions_agent = Agent.new(
|
21
|
+
# name: "ShopifyActions",
|
22
|
+
# instructions: "Perform Shopify operations",
|
23
|
+
# tools: [shopify_tool]
|
24
|
+
# )
|
25
|
+
#
|
26
|
+
# copilot = Agent.new(
|
27
|
+
# name: "SupportCopilot",
|
28
|
+
# tools: [
|
29
|
+
# conversation_agent.as_tool(
|
30
|
+
# name: "analyze_conversation",
|
31
|
+
# description: "Extract key info from conversation history"
|
32
|
+
# ),
|
33
|
+
# actions_agent.as_tool(
|
34
|
+
# name: "shopify_action",
|
35
|
+
# description: "Perform Shopify operations like refunds"
|
36
|
+
# )
|
37
|
+
# ]
|
38
|
+
# )
|
39
|
+
class AgentTool < Tool
|
40
|
+
attr_reader :wrapped_agent, :tool_name, :tool_description, :output_extractor
|
41
|
+
|
42
|
+
# Default parameter for agent tools
|
43
|
+
param :input, type: "string", desc: "Input message for the agent"
|
44
|
+
|
45
|
+
# Initialize an AgentTool that wraps an agent as a callable tool
|
46
|
+
#
|
47
|
+
# @param agent [Agents::Agent] The agent to wrap as a tool
|
48
|
+
# @param name [String, nil] Override the tool name (defaults to snake_case agent name)
|
49
|
+
# @param description [String, nil] Override the tool description
|
50
|
+
# @param output_extractor [Proc, nil] Custom proc to extract/transform the agent's output
|
51
|
+
def initialize(agent:, name: nil, description: nil, output_extractor: nil)
|
52
|
+
@wrapped_agent = agent
|
53
|
+
@tool_name = name || transform_agent_name(agent.name)
|
54
|
+
@tool_description = description || "Execute #{agent.name} agent"
|
55
|
+
@output_extractor = output_extractor
|
56
|
+
|
57
|
+
super()
|
58
|
+
end
|
59
|
+
|
60
|
+
def name
|
61
|
+
@tool_name
|
62
|
+
end
|
63
|
+
|
64
|
+
def description
|
65
|
+
@tool_description
|
66
|
+
end
|
67
|
+
|
68
|
+
# Execute the wrapped agent with constraints to prevent handoffs and recursion
|
69
|
+
def perform(tool_context, input:)
|
70
|
+
# Create isolated context for the wrapped agent
|
71
|
+
isolated_context = create_isolated_context(tool_context.context)
|
72
|
+
|
73
|
+
# Execute with explicit constraints:
|
74
|
+
# 1. Empty registry prevents handoffs
|
75
|
+
# 2. Low max_turns prevents infinite loops
|
76
|
+
# 3. Isolated context prevents history access
|
77
|
+
result = Runner.new.run(
|
78
|
+
@wrapped_agent,
|
79
|
+
input,
|
80
|
+
context: isolated_context,
|
81
|
+
registry: {}, # CONSTRAINT: No handoffs allowed
|
82
|
+
max_turns: 3 # CONSTRAINT: Limited turns for tool execution
|
83
|
+
)
|
84
|
+
|
85
|
+
return "Agent execution failed: #{result.error.message}" if result.error
|
86
|
+
|
87
|
+
# Extract output
|
88
|
+
if @output_extractor
|
89
|
+
@output_extractor.call(result)
|
90
|
+
else
|
91
|
+
result.output || "No output from #{@wrapped_agent.name}"
|
92
|
+
end
|
93
|
+
rescue StandardError => e
|
94
|
+
"Error executing #{@wrapped_agent.name}: #{e.message}"
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def transform_agent_name(name)
|
100
|
+
name.downcase.gsub(/\s+/, "_").gsub(/[^a-z0-9_]/, "")
|
101
|
+
end
|
102
|
+
|
103
|
+
# Create isolated context that only shares state, not conversation artifacts
|
104
|
+
def create_isolated_context(parent_context)
|
105
|
+
isolated = {}
|
106
|
+
|
107
|
+
# Only share the state - everything else is conversation-specific
|
108
|
+
isolated[:state] = parent_context[:state] if parent_context[:state]
|
109
|
+
|
110
|
+
isolated
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/lib/agents/handoff.rb
CHANGED
@@ -12,35 +12,10 @@ module Agents
|
|
12
12
|
# 4. The tool signals the handoff through context
|
13
13
|
# 5. The Runner detects this and switches to the new agent
|
14
14
|
#
|
15
|
-
# ##
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
# During development, we discovered that LLMs could call the same handoff tool multiple times
|
20
|
-
# in a single response, leading to infinite loops:
|
21
|
-
#
|
22
|
-
# 1. User: "My internet isn't working but my account shows active"
|
23
|
-
# 2. Triage Agent hands off to Support Agent
|
24
|
-
# 3. Support Agent sees account info is needed, hands back to Triage Agent
|
25
|
-
# 4. Triage Agent sees technical issue, hands off to Support Agent again
|
26
|
-
# 5. This creates an infinite ping-pong loop
|
27
|
-
#
|
28
|
-
# ### Root Cause Analysis
|
29
|
-
# Unlike OpenAI's SDK which processes tool calls before execution, RubyLLM automatically
|
30
|
-
# executes all tool calls in a response. This meant:
|
31
|
-
# - LLM calls handoff tool 10+ times in one response
|
32
|
-
# - Each call sets context[:pending_handoff], overwriting previous values
|
33
|
-
# - Runner processes handoffs after tool execution, seeing only the last one
|
34
|
-
# - Multiple handoff signals created conflicting state
|
35
|
-
#
|
36
|
-
# TODO: Overall, this problem can be tackled better if we replace the RubyLLM chat
|
37
|
-
# program with our own implementation.
|
38
|
-
#
|
39
|
-
# ### The Solution
|
40
|
-
# We implemented first-call-wins semantics inspired by OpenAI's approach:
|
41
|
-
# - First handoff call in a response sets the pending handoff
|
42
|
-
# - Subsequent calls are ignored with a "transfer in progress" message
|
43
|
-
# - This prevents loops and mirrors OpenAI SDK behavior
|
15
|
+
# ## Loop Prevention
|
16
|
+
# The library prevents infinite handoff loops by processing only the first handoff
|
17
|
+
# tool call in any LLM response. This is handled automatically by the Chat class
|
18
|
+
# which detects handoff tools and processes them separately from regular tools.
|
44
19
|
#
|
45
20
|
# ## Why Tools Instead of Instructions
|
46
21
|
# Using tools for handoffs has several advantages:
|
@@ -66,12 +41,11 @@ module Agents
|
|
66
41
|
# # LLM calls: handoff_to_billing()
|
67
42
|
# # Runner switches to billing_agent for the next turn
|
68
43
|
#
|
69
|
-
# @example
|
44
|
+
# @example Multiple handoff handling
|
70
45
|
# # Single LLM response with multiple handoff calls:
|
71
|
-
# # Call 1: handoff_to_support() ->
|
72
|
-
# # Call 2:
|
73
|
-
# #
|
74
|
-
# # Result: Only transfers to Support Agent (first call wins)
|
46
|
+
# # Call 1: handoff_to_support() -> Processed and executed
|
47
|
+
# # Call 2: handoff_to_billing() -> Ignored (only first handoff processed)
|
48
|
+
# # Result: Only transfers to Support Agent
|
75
49
|
class HandoffTool < Tool
|
76
50
|
attr_reader :target_agent
|
77
51
|
|
data/lib/agents/version.rb
CHANGED
data/lib/agents.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ai-agents
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shivam Mishra
|
@@ -37,7 +37,48 @@ files:
|
|
37
37
|
- LICENSE
|
38
38
|
- README.md
|
39
39
|
- Rakefile
|
40
|
+
- docs/Gemfile
|
41
|
+
- docs/Gemfile.lock
|
42
|
+
- docs/_config.yml
|
43
|
+
- docs/_sass/color_schemes/ruby.scss
|
44
|
+
- docs/_sass/custom/custom.scss
|
45
|
+
- docs/architecture.md
|
46
|
+
- docs/assets/fonts/InterVariable.woff2
|
47
|
+
- docs/concepts.md
|
48
|
+
- docs/concepts/agent-tool.md
|
49
|
+
- docs/concepts/agents.md
|
50
|
+
- docs/concepts/context.md
|
51
|
+
- docs/concepts/handoffs.md
|
52
|
+
- docs/concepts/runner.md
|
53
|
+
- docs/concepts/tools.md
|
54
|
+
- docs/guides.md
|
55
|
+
- docs/guides/agent-as-tool-pattern.md
|
56
|
+
- docs/guides/multi-agent-systems.md
|
57
|
+
- docs/guides/rails-integration.md
|
58
|
+
- docs/guides/state-persistence.md
|
59
|
+
- docs/index.md
|
40
60
|
- examples/README.md
|
61
|
+
- examples/collaborative-copilot/README.md
|
62
|
+
- examples/collaborative-copilot/agents/analysis_agent.rb
|
63
|
+
- examples/collaborative-copilot/agents/answer_suggestion_agent.rb
|
64
|
+
- examples/collaborative-copilot/agents/copilot_orchestrator.rb
|
65
|
+
- examples/collaborative-copilot/agents/integrations_agent.rb
|
66
|
+
- examples/collaborative-copilot/agents/research_agent.rb
|
67
|
+
- examples/collaborative-copilot/data/contacts.json
|
68
|
+
- examples/collaborative-copilot/data/conversations.json
|
69
|
+
- examples/collaborative-copilot/data/knowledge_base.json
|
70
|
+
- examples/collaborative-copilot/data/linear_issues.json
|
71
|
+
- examples/collaborative-copilot/data/stripe_billing.json
|
72
|
+
- examples/collaborative-copilot/interactive.rb
|
73
|
+
- examples/collaborative-copilot/tools/create_linear_ticket_tool.rb
|
74
|
+
- examples/collaborative-copilot/tools/get_article_tool.rb
|
75
|
+
- examples/collaborative-copilot/tools/get_contact_tool.rb
|
76
|
+
- examples/collaborative-copilot/tools/get_conversation_tool.rb
|
77
|
+
- examples/collaborative-copilot/tools/get_stripe_billing_tool.rb
|
78
|
+
- examples/collaborative-copilot/tools/search_contacts_tool.rb
|
79
|
+
- examples/collaborative-copilot/tools/search_conversations_tool.rb
|
80
|
+
- examples/collaborative-copilot/tools/search_knowledge_base_tool.rb
|
81
|
+
- examples/collaborative-copilot/tools/search_linear_issues_tool.rb
|
41
82
|
- examples/isp-support/README.md
|
42
83
|
- examples/isp-support/agents_factory.rb
|
43
84
|
- examples/isp-support/data/customers.json
|
@@ -52,6 +93,7 @@ files:
|
|
52
93
|
- lib/agents.rb
|
53
94
|
- lib/agents/agent.rb
|
54
95
|
- lib/agents/agent_runner.rb
|
96
|
+
- lib/agents/agent_tool.rb
|
55
97
|
- lib/agents/chat.rb
|
56
98
|
- lib/agents/handoff.rb
|
57
99
|
- lib/agents/result.rb
|