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,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "async"
5
+
6
+ module Google
7
+ module ADK
8
+ # Plugin base class for extending runner behavior
9
+ class Plugin
10
+ # Called when user sends a message
11
+ #
12
+ # @param context [InvocationContext] Current context
13
+ # @param message [String] User message
14
+ def on_user_message(context, message); end
15
+
16
+ # Called for each event
17
+ #
18
+ # @param context [InvocationContext] Current context
19
+ # @param event [Event] Current event
20
+ def on_event(context, event); end
21
+
22
+ # Called when agent starts
23
+ #
24
+ # @param context [InvocationContext] Current context
25
+ def on_agent_start(context); end
26
+
27
+ # Called when agent ends
28
+ #
29
+ # @param context [InvocationContext] Current context
30
+ def on_agent_end(context); end
31
+ end
32
+
33
+ # Main runner for orchestrating agent execution
34
+ class Runner
35
+ attr_reader :agent, :app_name, :session_service, :plugins
36
+
37
+ # Initialize a runner
38
+ #
39
+ # @param agent [BaseAgent] Root agent to run
40
+ # @param app_name [String] Application name
41
+ # @param session_service [BaseSessionService] Session service (optional)
42
+ # @param plugins [Array<Plugin>] Runner plugins (optional)
43
+ def initialize(agent:, app_name:, session_service: nil, plugins: [])
44
+ @agent = agent
45
+ @app_name = app_name
46
+ @session_service = session_service || InMemorySessionService.new
47
+ @plugins = plugins
48
+ end
49
+
50
+ # Run the agent synchronously
51
+ #
52
+ # @param user_id [String] User ID
53
+ # @param session_id [String] Session ID (optional)
54
+ # @param message [String] User message
55
+ # @param new_session [Boolean] Force new session (optional)
56
+ # @yield [Event] Events during execution
57
+ # @return [Enumerator<Event>] Event stream
58
+ def run(user_id:, message:, session_id: nil, new_session: false)
59
+ Enumerator.new do |yielder|
60
+ # Create or get session
61
+ session = if session_id && !new_session
62
+ @session_service.get_session(
63
+ app_name: @app_name,
64
+ user_id: user_id,
65
+ session_id: session_id
66
+ )
67
+ end
68
+
69
+ session ||= @session_service.create_session(
70
+ app_name: @app_name,
71
+ user_id: user_id
72
+ )
73
+
74
+ invocation_id = "inv-#{SecureRandom.uuid}"
75
+
76
+ # Create invocation context
77
+ context = InvocationContext.new(
78
+ session: session,
79
+ agent: @agent,
80
+ invocation_id: invocation_id,
81
+ session_service: @session_service
82
+ )
83
+
84
+ # Create and yield user event
85
+ user_event = Event.new(
86
+ invocation_id: invocation_id,
87
+ author: "user",
88
+ content: message
89
+ )
90
+
91
+ # Plugin callback
92
+ @plugins.each { |p| p.on_user_message(context, message) }
93
+
94
+ yielder << user_event
95
+ context.add_event(user_event)
96
+ @plugins.each { |p| p.on_event(context, user_event) }
97
+
98
+ # Update session
99
+ @session_service.append_event(
100
+ app_name: @app_name,
101
+ user_id: user_id,
102
+ session_id: session.id,
103
+ event: user_event
104
+ )
105
+
106
+ # Run agent
107
+ @plugins.each { |p| p.on_agent_start(context) }
108
+
109
+ begin
110
+ agent_events = @agent.run_async(message, context: context)
111
+
112
+ # Process agent events
113
+ agent_events.each do |event|
114
+ yielder << event
115
+ context.add_event(event)
116
+ @plugins.each { |p| p.on_event(context, event) }
117
+
118
+ # Update session with event
119
+ @session_service.append_event(
120
+ app_name: @app_name,
121
+ user_id: user_id,
122
+ session_id: session.id,
123
+ event: event
124
+ )
125
+
126
+ # Handle state updates from event actions
127
+ if event.actions&.state_delta && !event.actions.state_delta.empty?
128
+ @session_service.update_session(
129
+ app_name: @app_name,
130
+ user_id: user_id,
131
+ session_id: session.id,
132
+ state_updates: event.actions.state_delta
133
+ )
134
+ end
135
+
136
+ # Handle agent transfers
137
+ if event.actions&.transfer_to_agent
138
+ # In a full implementation, would transfer to another agent
139
+ break
140
+ end
141
+ end
142
+ rescue StandardError => e
143
+ # Create error event
144
+ error_event = Event.new(
145
+ invocation_id: invocation_id,
146
+ author: "system",
147
+ content: "Error: #{e.message}"
148
+ )
149
+ yielder << error_event
150
+ context.add_event(error_event)
151
+ ensure
152
+ @plugins.each { |p| p.on_agent_end(context) }
153
+ end
154
+ end
155
+ end
156
+
157
+ # Run the agent asynchronously
158
+ #
159
+ # @param user_id [String] User ID
160
+ # @param session_id [String] Session ID (optional)
161
+ # @param message [String] User message
162
+ # @param new_session [Boolean] Force new session (optional)
163
+ # @return [Enumerator<Event>] Event stream
164
+ def run_async(user_id:, message:, session_id: nil, new_session: false)
165
+ # In this simplified version, we delegate to the sync method
166
+ # In a full implementation, this would use Async gem for true async
167
+ run(
168
+ user_id: user_id,
169
+ message: message,
170
+ session_id: session_id,
171
+ new_session: new_session
172
+ )
173
+ end
174
+
175
+ # Run in live/streaming mode (experimental)
176
+ #
177
+ # @param user_id [String] User ID
178
+ # @param session_id [String] Session ID
179
+ def run_live(user_id:, session_id:)
180
+ raise NotImplementedError, "Live mode not yet implemented"
181
+ end
182
+
183
+ # Rewind session to previous state
184
+ #
185
+ # @param user_id [String] User ID
186
+ # @param session_id [String] Session ID
187
+ # @param invocation_id [String] Invocation to rewind before
188
+ def rewind_async(user_id:, session_id:, invocation_id:)
189
+ raise NotImplementedError, "Rewind not yet implemented"
190
+ end
191
+ end
192
+
193
+ # In-memory runner with built-in session service
194
+ class InMemoryRunner < Runner
195
+ # Initialize in-memory runner
196
+ #
197
+ # @param agent [BaseAgent] Root agent
198
+ # @param app_name [String] Application name
199
+ # @param plugins [Array<Plugin>] Runner plugins (optional)
200
+ def initialize(agent:, app_name:, plugins: [])
201
+ super(
202
+ agent: agent,
203
+ app_name: app_name,
204
+ session_service: InMemorySessionService.new,
205
+ plugins: plugins
206
+ )
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "time"
5
+
6
+ module Google
7
+ module ADK
8
+ # Represents a conversation session
9
+ class Session
10
+ attr_accessor :id, :app_name, :user_id, :state, :events, :last_update_time
11
+
12
+ # Initialize a session
13
+ #
14
+ # @param id [String] Session ID
15
+ # @param app_name [String] Application name
16
+ # @param user_id [String] User ID
17
+ # @param state [Hash] Session state (default: {})
18
+ # @param events [Array<Event>] Session events (default: [])
19
+ # @param last_update_time [Time] Last update timestamp (default: current time)
20
+ def initialize(id:, app_name:, user_id:, state: {}, events: [], last_update_time: nil)
21
+ @id = id
22
+ @app_name = app_name
23
+ @user_id = user_id
24
+ @state = state
25
+ @events = events
26
+ @last_update_time = last_update_time || Time.now
27
+ end
28
+
29
+ # Convert to hash representation
30
+ #
31
+ # @return [Hash] Hash representation
32
+ def to_h
33
+ {
34
+ id: @id,
35
+ app_name: @app_name,
36
+ user_id: @user_id,
37
+ state: @state,
38
+ events: @events.map(&:to_h),
39
+ last_update_time: @last_update_time.iso8601
40
+ }
41
+ end
42
+
43
+ # Create session from hash
44
+ #
45
+ # @param hash [Hash] Hash representation
46
+ # @return [Session] New session instance
47
+ def self.from_h(hash)
48
+ new(
49
+ id: hash[:id],
50
+ app_name: hash[:app_name],
51
+ user_id: hash[:user_id],
52
+ state: hash[:state] || {},
53
+ events: (hash[:events] || []).map { |e| Event.new(**e) },
54
+ last_update_time: Time.parse(hash[:last_update_time])
55
+ )
56
+ end
57
+ end
58
+
59
+ # Base class for session services
60
+ class BaseSessionService
61
+ # Create a new session
62
+ #
63
+ # @param app_name [String] Application name
64
+ # @param user_id [String] User ID
65
+ # @param initial_state [Hash] Initial state (optional)
66
+ # @return [Session] Created session
67
+ def create_session(app_name: nil, user_id: nil, initial_state: nil)
68
+ raise NotImplementedError, "Subclasses must implement #create_session"
69
+ end
70
+
71
+ # Get a session
72
+ #
73
+ # @param app_name [String] Application name
74
+ # @param user_id [String] User ID
75
+ # @param session_id [String] Session ID
76
+ # @return [Session, nil] Session or nil if not found
77
+ def get_session(app_name: nil, user_id: nil, session_id: nil)
78
+ raise NotImplementedError, "Subclasses must implement #get_session"
79
+ end
80
+
81
+ # Update session state
82
+ #
83
+ # @param app_name [String] Application name
84
+ # @param user_id [String] User ID
85
+ # @param session_id [String] Session ID
86
+ # @param state_updates [Hash] State updates
87
+ # @return [Session, nil] Updated session or nil if not found
88
+ def update_session(app_name: nil, user_id: nil, session_id: nil, state_updates: nil)
89
+ raise NotImplementedError, "Subclasses must implement #update_session"
90
+ end
91
+
92
+ # Append event to session
93
+ #
94
+ # @param app_name [String] Application name
95
+ # @param user_id [String] User ID
96
+ # @param session_id [String] Session ID
97
+ # @param event [Event] Event to append
98
+ # @return [Session, nil] Updated session or nil if not found
99
+ def append_event(app_name: nil, user_id: nil, session_id: nil, event: nil)
100
+ raise NotImplementedError, "Subclasses must implement #append_event"
101
+ end
102
+
103
+ # Delete a session
104
+ #
105
+ # @param app_name [String] Application name
106
+ # @param user_id [String] User ID
107
+ # @param session_id [String] Session ID
108
+ # @return [Boolean] True if deleted, false otherwise
109
+ def delete_session(app_name: nil, user_id: nil, session_id: nil)
110
+ raise NotImplementedError, "Subclasses must implement #delete_session"
111
+ end
112
+
113
+ # List sessions for a user
114
+ #
115
+ # @param app_name [String] Application name
116
+ # @param user_id [String] User ID
117
+ # @return [Array<Session>] List of sessions
118
+ def list_sessions(app_name: nil, user_id: nil)
119
+ raise NotImplementedError, "Subclasses must implement #list_sessions"
120
+ end
121
+ end
122
+
123
+ # In-memory session service for development/testing
124
+ class InMemorySessionService < BaseSessionService
125
+ def initialize
126
+ # Structure: { app_name => { user_id => { session_id => session } } }
127
+ @sessions = {}
128
+ @user_state = {}
129
+ @app_state = {}
130
+ end
131
+
132
+ # Create a new session
133
+ #
134
+ # @param app_name [String] Application name
135
+ # @param user_id [String] User ID
136
+ # @param initial_state [Hash] Initial state (optional)
137
+ # @return [Session] Created session
138
+ def create_session(app_name:, user_id:, initial_state: nil)
139
+ session = Session.new(
140
+ id: "session-#{SecureRandom.uuid}",
141
+ app_name: app_name,
142
+ user_id: user_id,
143
+ state: initial_state || {}
144
+ )
145
+
146
+ # Ensure nested structure exists
147
+ @sessions[app_name] ||= {}
148
+ @sessions[app_name][user_id] ||= {}
149
+ @sessions[app_name][user_id][session.id] = session
150
+
151
+ session
152
+ end
153
+
154
+ # Get a session
155
+ #
156
+ # @param app_name [String] Application name
157
+ # @param user_id [String] User ID
158
+ # @param session_id [String] Session ID
159
+ # @return [Session, nil] Session or nil if not found
160
+ def get_session(app_name:, user_id:, session_id:)
161
+ @sessions.dig(app_name, user_id, session_id)
162
+ end
163
+
164
+ # Update session state
165
+ #
166
+ # @param app_name [String] Application name
167
+ # @param user_id [String] User ID
168
+ # @param session_id [String] Session ID
169
+ # @param state_updates [Hash] State updates
170
+ # @return [Session, nil] Updated session or nil if not found
171
+ def update_session(app_name:, user_id:, session_id:, state_updates:)
172
+ session = get_session(app_name: app_name, user_id: user_id, session_id: session_id)
173
+ return nil unless session
174
+
175
+ session.state.merge!(state_updates)
176
+ session.last_update_time = Time.now
177
+ session
178
+ end
179
+
180
+ # Append event to session
181
+ #
182
+ # @param app_name [String] Application name
183
+ # @param user_id [String] User ID
184
+ # @param session_id [String] Session ID
185
+ # @param event [Event] Event to append
186
+ # @return [Session, nil] Updated session or nil if not found
187
+ def append_event(app_name:, user_id:, session_id:, event:)
188
+ session = get_session(app_name: app_name, user_id: user_id, session_id: session_id)
189
+ return nil unless session
190
+
191
+ session.events << event
192
+ session.last_update_time = Time.now
193
+ session
194
+ end
195
+
196
+ # Delete a session
197
+ #
198
+ # @param app_name [String] Application name
199
+ # @param user_id [String] User ID
200
+ # @param session_id [String] Session ID
201
+ # @return [Boolean] True if deleted, false otherwise
202
+ def delete_session(app_name:, user_id:, session_id:)
203
+ return false unless @sessions.dig(app_name, user_id, session_id)
204
+
205
+ @sessions[app_name][user_id].delete(session_id)
206
+ true
207
+ end
208
+
209
+ # List sessions for a user
210
+ #
211
+ # @param app_name [String] Application name
212
+ # @param user_id [String] User ID
213
+ # @return [Array<Session>] List of sessions
214
+ def list_sessions(app_name:, user_id:)
215
+ sessions_hash = @sessions.dig(app_name, user_id)
216
+ return [] unless sessions_hash
217
+
218
+ sessions_hash.values
219
+ end
220
+
221
+ # Get user state
222
+ #
223
+ # @param app_name [String] Application name
224
+ # @param user_id [String] User ID
225
+ # @return [Hash] User state
226
+ def get_user_state(app_name:, user_id:)
227
+ @user_state.dig(app_name, user_id) || {}
228
+ end
229
+
230
+ # Update user state
231
+ #
232
+ # @param app_name [String] Application name
233
+ # @param user_id [String] User ID
234
+ # @param state_updates [Hash] State updates
235
+ # @return [Hash] Updated user state
236
+ def update_user_state(app_name:, user_id:, state_updates:)
237
+ @user_state[app_name] ||= {}
238
+ @user_state[app_name][user_id] ||= {}
239
+ @user_state[app_name][user_id].merge!(state_updates)
240
+ end
241
+
242
+ # Get app state
243
+ #
244
+ # @param app_name [String] Application name
245
+ # @return [Hash] App state
246
+ def get_app_state(app_name:)
247
+ @app_state[app_name] || {}
248
+ end
249
+
250
+ # Update app state
251
+ #
252
+ # @param app_name [String] Application name
253
+ # @param state_updates [Hash] State updates
254
+ # @return [Hash] Updated app state
255
+ def update_app_state(app_name:, state_updates:)
256
+ @app_state[app_name] ||= {}
257
+ @app_state[app_name].merge!(state_updates)
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module Google
6
+ module ADK
7
+ # Tool that wraps another agent
8
+ class AgentTool < BaseTool
9
+ attr_reader :agent
10
+
11
+ # Initialize an agent tool
12
+ #
13
+ # @param agent [BaseAgent] The agent to wrap
14
+ def initialize(agent:)
15
+ super(name: agent.name, description: agent.description)
16
+ @agent = agent
17
+ end
18
+
19
+ # Execute the wrapped agent
20
+ #
21
+ # @param params [Hash] Parameters (should include 'message')
22
+ # @return [Object] Agent response
23
+ def call(params = {})
24
+ message = params[:message] || params["message"] || ""
25
+ context = params[:context]
26
+
27
+ # In a real implementation, this would run the agent
28
+ # and collect its response
29
+ if @agent.respond_to?(:run_async)
30
+ # Collect all events from the agent
31
+ events = []
32
+ @agent.run_async(message, context: context).each do |event|
33
+ events << event
34
+ end
35
+
36
+ # Return the last event's content as the result
37
+ events.last&.content || "No response"
38
+ else
39
+ "Agent #{@agent.name} cannot be executed"
40
+ end
41
+ end
42
+
43
+ # Get parameter schema
44
+ #
45
+ # @return [Hash] JSON schema for agent invocation
46
+ def schema
47
+ {
48
+ "type" => "object",
49
+ "properties" => {
50
+ "message" => {
51
+ "type" => "string",
52
+ "description" => "Message to send to the agent"
53
+ }
54
+ },
55
+ "required" => ["message"]
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Google
4
+ module ADK
5
+ # Base class for all tools
6
+ class BaseTool
7
+ attr_reader :name, :description
8
+
9
+ # Initialize base tool
10
+ #
11
+ # @param name [String] Tool name (optional)
12
+ # @param description [String] Tool description (optional)
13
+ def initialize(name: nil, description: nil)
14
+ @name = name
15
+ @description = description
16
+ end
17
+
18
+ # Execute the tool with given parameters
19
+ #
20
+ # @param params [Hash] Tool parameters
21
+ # @return [Object] Tool result
22
+ def call(params = {})
23
+ raise NotImplementedError, "Subclasses must implement #call"
24
+ end
25
+
26
+ # Get the tool's parameter schema
27
+ #
28
+ # @return [Hash] JSON schema for parameters
29
+ def schema
30
+ raise NotImplementedError, "Subclasses must implement #schema"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_tool"
4
+
5
+ module Google
6
+ module ADK
7
+ # Tool that wraps a Ruby callable (Proc, Method, etc.)
8
+ class FunctionTool < BaseTool
9
+ attr_reader :callable, :parameters_schema
10
+
11
+ # Initialize a function tool
12
+ #
13
+ # @param name [String] Tool name
14
+ # @param description [String] Tool description (optional)
15
+ # @param callable [Proc, Method] The function to wrap
16
+ # @param parameters_schema [Hash] JSON schema for parameters (optional)
17
+ def initialize(name:, description: nil, callable:, parameters_schema: nil)
18
+ super(name: name, description: description)
19
+ @callable = callable
20
+ @parameters_schema = parameters_schema # Don't default here, let to_gemini_schema handle it
21
+ end
22
+
23
+ # Execute the wrapped function
24
+ #
25
+ # @param params [Hash] Function parameters
26
+ # @return [Object] Function result
27
+ def call(params = {})
28
+ if @callable.parameters.empty?
29
+ @callable.call
30
+ else
31
+ # Convert hash params to keyword arguments if the callable expects them
32
+ if expects_keyword_args?
33
+ @callable.call(**params)
34
+ else
35
+ @callable.call(params)
36
+ end
37
+ end
38
+ end
39
+
40
+ # Get parameter schema
41
+ #
42
+ # @return [Hash] JSON schema
43
+ def schema
44
+ @parameters_schema
45
+ end
46
+
47
+ # Convert to Gemini API schema format
48
+ #
49
+ # @return [Hash] Gemini function declaration
50
+ def to_gemini_schema
51
+ # Try to infer parameters from method signature if no schema provided
52
+ schema = @parameters_schema || infer_schema_from_callable
53
+
54
+ {
55
+ "name" => @name,
56
+ "description" => @description || "Function tool",
57
+ "parameters" => schema
58
+ }
59
+ end
60
+
61
+ private
62
+
63
+ # Check if callable expects keyword arguments
64
+ #
65
+ # @return [Boolean] True if expects kwargs
66
+ def expects_keyword_args?
67
+ @callable.parameters.any? { |type, _name| %i[key keyreq keyrest].include?(type) }
68
+ end
69
+
70
+ # Generate default schema
71
+ #
72
+ # @return [Hash] Default empty schema
73
+ def default_schema
74
+ {
75
+ "type" => "object",
76
+ "properties" => {},
77
+ "required" => []
78
+ }
79
+ end
80
+
81
+ # Infer schema from callable parameters
82
+ #
83
+ # @return [Hash] Inferred JSON schema
84
+ def infer_schema_from_callable
85
+ properties = {}
86
+ required = []
87
+
88
+ @callable.parameters.each do |type, name|
89
+ next if type == :block
90
+
91
+ param_name = name.to_s
92
+
93
+ # Determine type based on parameter name patterns
94
+ param_type = case param_name
95
+ when /amount|rate|fee|price|cost/
96
+ "number"
97
+ when /days|count|limit/
98
+ "integer"
99
+ else
100
+ "string"
101
+ end
102
+
103
+ properties[param_name] = {
104
+ "type" => param_type,
105
+ "description" => generate_param_description(param_name)
106
+ }
107
+
108
+ # Required if it's a required positional or keyword arg
109
+ if [:req, :keyreq].include?(type)
110
+ required << param_name
111
+ end
112
+ end
113
+
114
+ {
115
+ "type" => "object",
116
+ "properties" => properties,
117
+ "required" => required
118
+ }
119
+ end
120
+
121
+ # Generate descriptive parameter descriptions
122
+ def generate_param_description(param_name)
123
+ case param_name
124
+ when "from"
125
+ "Source currency code (e.g., USD, EUR, GBP)"
126
+ when "to"
127
+ "Target currency code (e.g., USD, EUR, GBP)"
128
+ when "amount"
129
+ "Amount of money to convert (number)"
130
+ when "city"
131
+ "City name for weather information"
132
+ when "days"
133
+ "Number of days for forecast (1-3)"
134
+ else
135
+ "Parameter: #{param_name}"
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end