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,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
|