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.
@@ -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