google-adk 0.1.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 +7 -0
- data/CHANGELOG.md +58 -0
- data/LICENSE.txt +21 -0
- data/README.md +193 -0
- data/google-adk.gemspec +51 -0
- data/lib/google/adk/agents/base_agent.rb +149 -0
- data/lib/google/adk/agents/llm_agent.rb +322 -0
- data/lib/google/adk/agents/simple_llm_agent.rb +67 -0
- data/lib/google/adk/agents/workflow_agents/loop_agent.rb +116 -0
- data/lib/google/adk/agents/workflow_agents/parallel_agent.rb +138 -0
- data/lib/google/adk/agents/workflow_agents/sequential_agent.rb +108 -0
- data/lib/google/adk/clients/gemini_client.rb +90 -0
- data/lib/google/adk/context.rb +241 -0
- data/lib/google/adk/events.rb +191 -0
- data/lib/google/adk/runner.rb +210 -0
- data/lib/google/adk/session.rb +261 -0
- data/lib/google/adk/tools/agent_tool.rb +60 -0
- data/lib/google/adk/tools/base_tool.rb +34 -0
- data/lib/google/adk/tools/function_tool.rb +140 -0
- data/lib/google/adk/version.rb +7 -0
- data/lib/google/adk.rb +30 -0
- data/lib/google-adk.rb +3 -0
- metadata +253 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require_relative "../tools/base_tool"
|
|
5
|
+
require_relative "../tools/function_tool"
|
|
6
|
+
require_relative "../tools/agent_tool"
|
|
7
|
+
require_relative "../clients/gemini_client"
|
|
8
|
+
|
|
9
|
+
module Google
|
|
10
|
+
module ADK
|
|
11
|
+
# LLM-powered agent that can use tools and interact with language models
|
|
12
|
+
class LlmAgent < BaseAgent
|
|
13
|
+
attr_reader :model, :instructions, :tools, :include_from_children,
|
|
14
|
+
:inherit_parent_model, :before_model_callback, :after_model_callback,
|
|
15
|
+
:before_tool_callback, :after_tool_callback, :code_executor, :planner
|
|
16
|
+
|
|
17
|
+
# Initialize an LLM agent
|
|
18
|
+
#
|
|
19
|
+
# @param name [String] Agent name
|
|
20
|
+
# @param model [String, nil] Model name (e.g., "gemini-2.0-flash")
|
|
21
|
+
# @param instructions [String] System instructions (can include {variables})
|
|
22
|
+
# @param description [String] Agent description
|
|
23
|
+
# @param tools [Array] Tools available to the agent
|
|
24
|
+
# @param sub_agents [Array<BaseAgent>] Child agents
|
|
25
|
+
# @param include_from_children [Array<String>] What to include from children
|
|
26
|
+
# @param inherit_parent_model [Boolean] Whether to inherit parent's model
|
|
27
|
+
# @param before_model_callback [Proc] Called before model invocation
|
|
28
|
+
# @param after_model_callback [Proc] Called after model invocation
|
|
29
|
+
# @param before_tool_callback [Proc] Called before tool execution
|
|
30
|
+
# @param after_tool_callback [Proc] Called after tool execution
|
|
31
|
+
# @param code_executor [Object] Optional code executor
|
|
32
|
+
# @param planner [Object] Optional planner
|
|
33
|
+
# @raise [ArgumentError] If model is required but not provided
|
|
34
|
+
def initialize(name:, model: nil, instructions: nil, description: nil,
|
|
35
|
+
tools: [], sub_agents: [], include_from_children: [],
|
|
36
|
+
inherit_parent_model: false,
|
|
37
|
+
before_model_callback: nil, after_model_callback: nil,
|
|
38
|
+
before_tool_callback: nil, after_tool_callback: nil,
|
|
39
|
+
before_agent_callback: nil, after_agent_callback: nil,
|
|
40
|
+
code_executor: nil, planner: nil)
|
|
41
|
+
super(
|
|
42
|
+
name: name,
|
|
43
|
+
description: description,
|
|
44
|
+
sub_agents: sub_agents,
|
|
45
|
+
before_agent_callback: before_agent_callback,
|
|
46
|
+
after_agent_callback: after_agent_callback
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
raise ArgumentError, "model is required" if model.nil? && !inherit_parent_model
|
|
50
|
+
|
|
51
|
+
@model = model
|
|
52
|
+
@instructions = instructions
|
|
53
|
+
@tools = tools
|
|
54
|
+
@include_from_children = include_from_children
|
|
55
|
+
@inherit_parent_model = inherit_parent_model
|
|
56
|
+
@before_model_callback = before_model_callback
|
|
57
|
+
@after_model_callback = after_model_callback
|
|
58
|
+
@before_tool_callback = before_tool_callback
|
|
59
|
+
@after_tool_callback = after_tool_callback
|
|
60
|
+
@code_executor = code_executor
|
|
61
|
+
@planner = planner
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get the canonical model to use
|
|
65
|
+
#
|
|
66
|
+
# @return [String] Model name
|
|
67
|
+
# @raise [ConfigurationError] If no model is available
|
|
68
|
+
def canonical_model
|
|
69
|
+
return @model if @model
|
|
70
|
+
|
|
71
|
+
if @inherit_parent_model && @parent_agent&.respond_to?(:canonical_model)
|
|
72
|
+
return @parent_agent.canonical_model
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
raise ConfigurationError, "No model specified for agent #{@name}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Get canonical instructions with variables interpolated
|
|
79
|
+
#
|
|
80
|
+
# @param context [Context] Current context
|
|
81
|
+
# @return [String] Processed instructions
|
|
82
|
+
def canonical_instructions(context)
|
|
83
|
+
return "" unless @instructions
|
|
84
|
+
|
|
85
|
+
# Simple variable interpolation from state
|
|
86
|
+
processed = @instructions.dup
|
|
87
|
+
context.state.each do |key, value|
|
|
88
|
+
processed.gsub!("{#{key}}", value.to_s)
|
|
89
|
+
end
|
|
90
|
+
processed
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Get canonical tools (converted to proper tool objects)
|
|
94
|
+
#
|
|
95
|
+
# @return [Array<BaseTool>] Tool objects
|
|
96
|
+
def canonical_tools
|
|
97
|
+
@tools.map do |tool|
|
|
98
|
+
case tool
|
|
99
|
+
when BaseTool
|
|
100
|
+
tool
|
|
101
|
+
when BaseAgent
|
|
102
|
+
AgentTool.new(agent: tool)
|
|
103
|
+
else
|
|
104
|
+
# Assume it's a callable (proc/method)
|
|
105
|
+
FunctionTool.new(
|
|
106
|
+
name: tool_name_from_callable(tool),
|
|
107
|
+
description: "Function tool",
|
|
108
|
+
callable: tool
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Implementation of async run
|
|
115
|
+
#
|
|
116
|
+
# @param message [String] User message
|
|
117
|
+
# @param context [InvocationContext] Invocation context
|
|
118
|
+
# @yield [Event] Events during execution
|
|
119
|
+
def run_async(message, context: nil)
|
|
120
|
+
Enumerator.new do |yielder|
|
|
121
|
+
begin
|
|
122
|
+
# Initialize Gemini client
|
|
123
|
+
client = GeminiClient.new
|
|
124
|
+
|
|
125
|
+
# Build simple message for now
|
|
126
|
+
messages = [{ role: "user", content: message }]
|
|
127
|
+
|
|
128
|
+
# Get tools and convert to Gemini format
|
|
129
|
+
tools = canonical_tools.map(&:to_gemini_schema)
|
|
130
|
+
|
|
131
|
+
# Create more forceful system instruction for tools
|
|
132
|
+
system_instruction = build_tool_aware_instructions(context, tools)
|
|
133
|
+
|
|
134
|
+
# Call Gemini API with tools
|
|
135
|
+
response = client.generate_content(
|
|
136
|
+
model: canonical_model,
|
|
137
|
+
messages: messages,
|
|
138
|
+
tools: tools.empty? ? nil : tools,
|
|
139
|
+
system_instruction: system_instruction
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Process response
|
|
143
|
+
if response.dig("candidates", 0, "content", "parts")
|
|
144
|
+
parts = response["candidates"][0]["content"]["parts"]
|
|
145
|
+
|
|
146
|
+
parts.each do |part|
|
|
147
|
+
if part["text"]
|
|
148
|
+
# Regular text response
|
|
149
|
+
event = Event.new(
|
|
150
|
+
invocation_id: context&.invocation_id || "inv-#{SecureRandom.uuid}",
|
|
151
|
+
author: @name,
|
|
152
|
+
content: part["text"]
|
|
153
|
+
)
|
|
154
|
+
yielder << event
|
|
155
|
+
context&.add_event(event) if context
|
|
156
|
+
|
|
157
|
+
elsif part["functionCall"]
|
|
158
|
+
# Tool call - execute and get result
|
|
159
|
+
function_call = part["functionCall"]
|
|
160
|
+
tool_name = function_call["name"]
|
|
161
|
+
tool_args = function_call["args"] || {}
|
|
162
|
+
|
|
163
|
+
# Execute the tool
|
|
164
|
+
tool_result = execute_tool_call(tool_name, tool_args, yielder, context)
|
|
165
|
+
|
|
166
|
+
# Call LLM again with tool result
|
|
167
|
+
tool_messages = messages + [
|
|
168
|
+
{
|
|
169
|
+
role: "model",
|
|
170
|
+
parts: [{ functionCall: function_call }]
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
role: "function",
|
|
174
|
+
parts: [{
|
|
175
|
+
functionResponse: {
|
|
176
|
+
name: tool_name,
|
|
177
|
+
response: tool_result
|
|
178
|
+
}
|
|
179
|
+
}]
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
follow_up_response = client.generate_content(
|
|
184
|
+
model: canonical_model,
|
|
185
|
+
messages: tool_messages,
|
|
186
|
+
system_instruction: system_instruction
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if follow_up_response.dig("candidates", 0, "content", "parts", 0, "text")
|
|
190
|
+
final_text = follow_up_response["candidates"][0]["content"]["parts"][0]["text"]
|
|
191
|
+
event = Event.new(
|
|
192
|
+
invocation_id: context&.invocation_id || "inv-#{SecureRandom.uuid}",
|
|
193
|
+
author: @name,
|
|
194
|
+
content: final_text
|
|
195
|
+
)
|
|
196
|
+
yielder << event
|
|
197
|
+
context&.add_event(event) if context
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
else
|
|
202
|
+
# Fallback response
|
|
203
|
+
event = Event.new(
|
|
204
|
+
invocation_id: context&.invocation_id || "inv-#{SecureRandom.uuid}",
|
|
205
|
+
author: @name,
|
|
206
|
+
content: "I'm sorry, I couldn't process that request."
|
|
207
|
+
)
|
|
208
|
+
yielder << event
|
|
209
|
+
context&.add_event(event) if context
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
rescue => e
|
|
213
|
+
# Error handling
|
|
214
|
+
puts "[DEBUG] Gemini error: #{e.message}" if ENV["DEBUG"]
|
|
215
|
+
puts "[DEBUG] Backtrace: #{e.backtrace.first(3).join(', ')}" if ENV["DEBUG"]
|
|
216
|
+
|
|
217
|
+
event = Event.new(
|
|
218
|
+
invocation_id: context&.invocation_id || "inv-#{SecureRandom.uuid}",
|
|
219
|
+
author: @name,
|
|
220
|
+
content: "Error calling Gemini API: #{e.message}. Please check your GEMINI_API_KEY."
|
|
221
|
+
)
|
|
222
|
+
yielder << event
|
|
223
|
+
context&.add_event(event) if context
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
private
|
|
229
|
+
|
|
230
|
+
# Build tool-aware system instructions
|
|
231
|
+
def build_tool_aware_instructions(context, tools)
|
|
232
|
+
base_instructions = canonical_instructions(context) || ""
|
|
233
|
+
|
|
234
|
+
if tools.empty?
|
|
235
|
+
return base_instructions
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
tool_instructions = <<~TOOL_INSTRUCTIONS
|
|
239
|
+
|
|
240
|
+
IMPORTANT: You have access to the following tools. You MUST use these tools when the user asks for information that requires them:
|
|
241
|
+
|
|
242
|
+
TOOL_INSTRUCTIONS
|
|
243
|
+
|
|
244
|
+
tools.each do |tool|
|
|
245
|
+
tool_instructions += "- #{tool['name']}: #{tool['description']}\n"
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
tool_instructions += <<~GUIDELINES
|
|
249
|
+
|
|
250
|
+
GUIDELINES FOR TOOL USAGE:
|
|
251
|
+
1. When a user asks for currency conversion, exchange rates, or related information, you MUST use the appropriate currency tools
|
|
252
|
+
2. When a user asks for weather information, you MUST use the appropriate weather tools
|
|
253
|
+
3. Always call the most relevant tool first, then provide a helpful response based on the results
|
|
254
|
+
4. If a tool returns an error, explain what happened and suggest alternatives
|
|
255
|
+
5. Format tool results in a user-friendly way
|
|
256
|
+
GUIDELINES
|
|
257
|
+
|
|
258
|
+
base_instructions + tool_instructions
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Build conversation history from context
|
|
262
|
+
def build_conversation_history(context)
|
|
263
|
+
return [] unless context&.session
|
|
264
|
+
|
|
265
|
+
# Convert session events to message format
|
|
266
|
+
messages = []
|
|
267
|
+
context.session.events.each do |event|
|
|
268
|
+
if event.author == "user"
|
|
269
|
+
messages << { role: "user", content: event.content }
|
|
270
|
+
elsif event.author == @name
|
|
271
|
+
messages << { role: "assistant", content: event.content }
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
messages
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Execute a tool call
|
|
278
|
+
def execute_tool_call(tool_name, tool_args, yielder, context)
|
|
279
|
+
puts "[DEBUG] Executing tool: #{tool_name} with args: #{tool_args}" if ENV["DEBUG"]
|
|
280
|
+
|
|
281
|
+
# Find the tool
|
|
282
|
+
tool = canonical_tools.find { |t| t.name == tool_name }
|
|
283
|
+
unless tool
|
|
284
|
+
return { error: "Tool not found: #{tool_name}" }
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
begin
|
|
288
|
+
# Convert string keys to symbols and fix parameter name issues
|
|
289
|
+
symbol_args = {}
|
|
290
|
+
tool_args.each do |k, v|
|
|
291
|
+
# Handle Gemini's parameter name quirks
|
|
292
|
+
clean_key = k.to_s.gsub(/\d+_$/, '') # Remove trailing numbers and underscores
|
|
293
|
+
symbol_args[clean_key.to_sym] = v
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Execute the tool
|
|
297
|
+
result = tool.call(symbol_args)
|
|
298
|
+
puts "[DEBUG] Tool result: #{result}" if ENV["DEBUG"]
|
|
299
|
+
result
|
|
300
|
+
rescue => e
|
|
301
|
+
puts "[DEBUG] Tool error: #{e.message}" if ENV["DEBUG"]
|
|
302
|
+
{ error: "Tool error: #{e.message}" }
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Extract a reasonable name from a callable
|
|
307
|
+
#
|
|
308
|
+
# @param callable [Proc, Method] Callable object
|
|
309
|
+
# @return [String] Tool name
|
|
310
|
+
def tool_name_from_callable(callable)
|
|
311
|
+
case callable
|
|
312
|
+
when Proc
|
|
313
|
+
"function_#{callable.object_id}"
|
|
314
|
+
when Method
|
|
315
|
+
callable.name.to_s
|
|
316
|
+
else
|
|
317
|
+
"tool_#{callable.object_id}"
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../clients/gemini_client"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module Google
|
|
7
|
+
module ADK
|
|
8
|
+
# Simplified LLM agent with actual Gemini integration
|
|
9
|
+
class SimpleLlmAgent
|
|
10
|
+
attr_reader :model, :name, :instructions, :tools
|
|
11
|
+
|
|
12
|
+
def initialize(model:, name:, instructions: nil, tools: [])
|
|
13
|
+
@model = model
|
|
14
|
+
@name = name
|
|
15
|
+
@instructions = instructions
|
|
16
|
+
@tools = tools
|
|
17
|
+
@client = GeminiClient.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Simple synchronous call to Gemini
|
|
21
|
+
def call(message)
|
|
22
|
+
begin
|
|
23
|
+
# Simple call without tools for now
|
|
24
|
+
response = @client.generate_content(
|
|
25
|
+
model: @model,
|
|
26
|
+
messages: [{ role: "user", content: message }],
|
|
27
|
+
system_instruction: @instructions
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if response.dig("candidates", 0, "content", "parts", 0, "text")
|
|
31
|
+
response["candidates"][0]["content"]["parts"][0]["text"]
|
|
32
|
+
else
|
|
33
|
+
"I apologize, but I couldn't generate a response."
|
|
34
|
+
end
|
|
35
|
+
rescue => e
|
|
36
|
+
"Error: #{e.message}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Generate events for the runner
|
|
41
|
+
def run_async(message, context: nil)
|
|
42
|
+
Enumerator.new do |yielder|
|
|
43
|
+
begin
|
|
44
|
+
response_text = call(message)
|
|
45
|
+
|
|
46
|
+
event = Event.new(
|
|
47
|
+
invocation_id: context&.invocation_id || "inv-#{SecureRandom.uuid}",
|
|
48
|
+
author: @name,
|
|
49
|
+
content: response_text
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
yielder << event
|
|
53
|
+
context&.add_event(event) if context
|
|
54
|
+
|
|
55
|
+
rescue => e
|
|
56
|
+
error_event = Event.new(
|
|
57
|
+
invocation_id: context&.invocation_id || "inv-#{SecureRandom.uuid}",
|
|
58
|
+
author: @name,
|
|
59
|
+
content: "Error: #{e.message}"
|
|
60
|
+
)
|
|
61
|
+
yielder << error_event
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Google
|
|
4
|
+
module ADK
|
|
5
|
+
# Agent that executes another agent in a loop
|
|
6
|
+
class LoopAgent < BaseAgent
|
|
7
|
+
attr_reader :agent, :loop_condition, :max_iterations
|
|
8
|
+
|
|
9
|
+
# Initialize a loop agent
|
|
10
|
+
#
|
|
11
|
+
# @param name [String] Agent name
|
|
12
|
+
# @param description [String] Agent description (optional)
|
|
13
|
+
# @param agent [BaseAgent] Agent to execute in loop
|
|
14
|
+
# @param loop_condition [Proc] Condition to continue loop (optional)
|
|
15
|
+
# @param max_iterations [Integer] Maximum iterations (default: 10)
|
|
16
|
+
# @param before_agent_callback [Proc] Callback before agent execution
|
|
17
|
+
# @param after_agent_callback [Proc] Callback after agent execution
|
|
18
|
+
def initialize(name:, agent:, description: nil, loop_condition: nil,
|
|
19
|
+
max_iterations: 10, before_agent_callback: nil,
|
|
20
|
+
after_agent_callback: nil)
|
|
21
|
+
super(
|
|
22
|
+
name: name,
|
|
23
|
+
description: description || "Executes #{agent.name} in a loop",
|
|
24
|
+
sub_agents: [agent],
|
|
25
|
+
before_agent_callback: before_agent_callback,
|
|
26
|
+
after_agent_callback: after_agent_callback
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@agent = agent
|
|
30
|
+
@loop_condition = loop_condition
|
|
31
|
+
@max_iterations = max_iterations
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Run agent in a loop
|
|
35
|
+
#
|
|
36
|
+
# @param message [String] Initial message
|
|
37
|
+
# @param context [InvocationContext] Invocation context
|
|
38
|
+
# @yield [Event] Events during execution
|
|
39
|
+
def run_async(message, context: nil)
|
|
40
|
+
Enumerator.new do |yielder|
|
|
41
|
+
invocation_id = context&.invocation_id || "loop-#{SecureRandom.uuid}"
|
|
42
|
+
|
|
43
|
+
# Yield start event
|
|
44
|
+
start_event = Event.new(
|
|
45
|
+
invocation_id: invocation_id,
|
|
46
|
+
author: @name,
|
|
47
|
+
content: "Starting loop execution with agent #{@agent.name}"
|
|
48
|
+
)
|
|
49
|
+
yielder << start_event
|
|
50
|
+
|
|
51
|
+
current_input = message
|
|
52
|
+
iteration = 0
|
|
53
|
+
last_result = nil
|
|
54
|
+
|
|
55
|
+
# Loop while condition is met or until max iterations
|
|
56
|
+
while iteration < @max_iterations
|
|
57
|
+
# Check loop condition if provided
|
|
58
|
+
if @loop_condition && !@loop_condition.call(last_result, iteration)
|
|
59
|
+
break
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Yield iteration event
|
|
63
|
+
iteration_event = Event.new(
|
|
64
|
+
invocation_id: invocation_id,
|
|
65
|
+
author: @name,
|
|
66
|
+
content: "Iteration #{iteration + 1}/#{@max_iterations}"
|
|
67
|
+
)
|
|
68
|
+
yielder << iteration_event
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
# Run the agent
|
|
72
|
+
agent_output = nil
|
|
73
|
+
if @agent.respond_to?(:run_async)
|
|
74
|
+
@agent.run_async(current_input, context: context).each do |event|
|
|
75
|
+
yielder << event
|
|
76
|
+
# Capture last content as result
|
|
77
|
+
agent_output = event.content if event.content
|
|
78
|
+
end
|
|
79
|
+
else
|
|
80
|
+
error_event = Event.new(
|
|
81
|
+
invocation_id: invocation_id,
|
|
82
|
+
author: @name,
|
|
83
|
+
content: "Agent #{@agent.name} does not implement run_async"
|
|
84
|
+
)
|
|
85
|
+
yielder << error_event
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Update for next iteration
|
|
89
|
+
last_result = agent_output || current_input
|
|
90
|
+
current_input = last_result
|
|
91
|
+
iteration += 1
|
|
92
|
+
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
# Handle errors
|
|
95
|
+
error_event = Event.new(
|
|
96
|
+
invocation_id: invocation_id,
|
|
97
|
+
author: @name,
|
|
98
|
+
content: "Error in iteration #{iteration + 1}: #{e.message}"
|
|
99
|
+
)
|
|
100
|
+
yielder << error_event
|
|
101
|
+
iteration += 1
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Yield completion event
|
|
106
|
+
end_event = Event.new(
|
|
107
|
+
invocation_id: invocation_id,
|
|
108
|
+
author: @name,
|
|
109
|
+
content: "Completed loop execution after #{iteration} iterations. Final result: #{last_result}"
|
|
110
|
+
)
|
|
111
|
+
yielder << end_event
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent-ruby"
|
|
4
|
+
|
|
5
|
+
module Google
|
|
6
|
+
module ADK
|
|
7
|
+
# Agent that executes sub-agents in parallel
|
|
8
|
+
class ParallelAgent < BaseAgent
|
|
9
|
+
attr_reader :agents, :aggregation_strategy
|
|
10
|
+
|
|
11
|
+
# Initialize a parallel agent
|
|
12
|
+
#
|
|
13
|
+
# @param name [String] Agent name
|
|
14
|
+
# @param description [String] Agent description (optional)
|
|
15
|
+
# @param agents [Array<BaseAgent>] Agents to execute in parallel
|
|
16
|
+
# @param aggregation_strategy [Symbol] How to aggregate results (:all, :first, :majority)
|
|
17
|
+
# @param before_agent_callback [Proc] Callback before agent execution
|
|
18
|
+
# @param after_agent_callback [Proc] Callback after agent execution
|
|
19
|
+
# @raise [ArgumentError] If no agents provided
|
|
20
|
+
def initialize(name:, agents:, description: nil, aggregation_strategy: :all,
|
|
21
|
+
before_agent_callback: nil, after_agent_callback: nil)
|
|
22
|
+
raise ArgumentError, "Parallel agent requires at least one agent" if agents.empty?
|
|
23
|
+
|
|
24
|
+
super(
|
|
25
|
+
name: name,
|
|
26
|
+
description: description || "Executes #{agents.length} agents in parallel",
|
|
27
|
+
sub_agents: agents,
|
|
28
|
+
before_agent_callback: before_agent_callback,
|
|
29
|
+
after_agent_callback: after_agent_callback
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@agents = agents
|
|
33
|
+
@aggregation_strategy = aggregation_strategy
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Run agents in parallel
|
|
37
|
+
#
|
|
38
|
+
# @param message [String] Message to send to all agents
|
|
39
|
+
# @param context [InvocationContext] Invocation context
|
|
40
|
+
# @yield [Event] Events during execution
|
|
41
|
+
def run_async(message, context: nil)
|
|
42
|
+
Enumerator.new do |yielder|
|
|
43
|
+
invocation_id = context&.invocation_id || "par-#{SecureRandom.uuid}"
|
|
44
|
+
|
|
45
|
+
# Yield start event
|
|
46
|
+
start_event = Event.new(
|
|
47
|
+
invocation_id: invocation_id,
|
|
48
|
+
author: @name,
|
|
49
|
+
content: "Starting parallel execution with #{@agents.length} agents"
|
|
50
|
+
)
|
|
51
|
+
yielder << start_event
|
|
52
|
+
|
|
53
|
+
# Collect results from all agents
|
|
54
|
+
agent_results = {}
|
|
55
|
+
failed_agents = []
|
|
56
|
+
|
|
57
|
+
# In a real async implementation, these would run concurrently
|
|
58
|
+
# For this simplified version, we'll run them sequentially
|
|
59
|
+
# but collect all results before aggregating
|
|
60
|
+
@agents.each do |agent|
|
|
61
|
+
begin
|
|
62
|
+
agent_events = []
|
|
63
|
+
|
|
64
|
+
if agent.respond_to?(:run_async)
|
|
65
|
+
agent.run_async(message, context: context).each do |event|
|
|
66
|
+
yielder << event
|
|
67
|
+
agent_events << event
|
|
68
|
+
end
|
|
69
|
+
else
|
|
70
|
+
# For agents that don't implement run_async
|
|
71
|
+
error_event = Event.new(
|
|
72
|
+
invocation_id: invocation_id,
|
|
73
|
+
author: @name,
|
|
74
|
+
content: "Agent #{agent.name} does not implement run_async"
|
|
75
|
+
)
|
|
76
|
+
yielder << error_event
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Store the last content event as the result
|
|
80
|
+
last_content = agent_events.reverse.find { |e| e.content }&.content
|
|
81
|
+
agent_results[agent.name] = last_content if last_content
|
|
82
|
+
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
# Track failed agents
|
|
85
|
+
failed_agents << agent.name
|
|
86
|
+
error_event = Event.new(
|
|
87
|
+
invocation_id: invocation_id,
|
|
88
|
+
author: @name,
|
|
89
|
+
content: "Agent #{agent.name} failed: #{e.message}"
|
|
90
|
+
)
|
|
91
|
+
yielder << error_event
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Aggregate results based on strategy
|
|
96
|
+
final_result = aggregate_results(agent_results, failed_agents)
|
|
97
|
+
|
|
98
|
+
# Yield completion event
|
|
99
|
+
end_event = Event.new(
|
|
100
|
+
invocation_id: invocation_id,
|
|
101
|
+
author: @name,
|
|
102
|
+
content: final_result
|
|
103
|
+
)
|
|
104
|
+
yielder << end_event
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
# Aggregate results from parallel execution
|
|
111
|
+
#
|
|
112
|
+
# @param results [Hash] Agent name => result mapping
|
|
113
|
+
# @param failed [Array] Names of failed agents
|
|
114
|
+
# @return [String] Aggregated result
|
|
115
|
+
def aggregate_results(results, failed)
|
|
116
|
+
case @aggregation_strategy
|
|
117
|
+
when :first
|
|
118
|
+
if results.any?
|
|
119
|
+
agent_name = results.keys.first
|
|
120
|
+
"Completed parallel execution using first agent result (#{agent_name}): #{results.values.first}"
|
|
121
|
+
else
|
|
122
|
+
"Completed parallel execution but no agents returned results"
|
|
123
|
+
end
|
|
124
|
+
when :all
|
|
125
|
+
if results.any?
|
|
126
|
+
result_summary = results.map { |name, result| "#{name}: #{result}" }.join("; ")
|
|
127
|
+
failed_summary = failed.any? ? " (#{failed.length} agents failed)" : ""
|
|
128
|
+
"Completed parallel execution. Results from #{results.length} agents: #{result_summary}#{failed_summary}"
|
|
129
|
+
else
|
|
130
|
+
"Completed parallel execution but no agents returned results"
|
|
131
|
+
end
|
|
132
|
+
else
|
|
133
|
+
"Completed parallel execution with #{results.length} results"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|