Skip to content

Commit 417a8bc

Browse files
Jacksunweicopybara-github
authored andcommitted
feat(config): Supports loading yaml agents in mvn google-adk@web ...
Usage: ``` mvn clean compile google-adk:web -Dagents=/path/to/agents ``` Where each subdirectory in agents with a `root_agent.yaml` file will be recognized an agent in ADK Web UI. PiperOrigin-RevId: 794342536
1 parent c0302b6 commit 417a8bc

File tree

4 files changed

+580
-8
lines changed

4 files changed

+580
-8
lines changed

maven_plugin/README.md

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ mvn google-adk:web -Dagents=com.example.MyAgentLoader.INSTANCE
1212

1313
### Parameters
1414

15-
- **`agents`** (required): Full class path to your AgentLoader implementation
15+
- **`agents`** (required): Full class path to your AgentLoader implementation OR path to agent configuration directory
1616
- **`port`** (optional, default: 8000): Port for the web server
1717
- **`host`** (optional, default: localhost): Host address to bind to
1818
- **`hotReloading`** (optional, default: true): Whether to enable hot reloading
@@ -195,6 +195,59 @@ Usage:
195195
mvn google-adk:web -Dagents=com.example.MultipleLoaders.ADVANCED
196196
```
197197

198+
## Config-Based Agents (Directory Path)
199+
200+
For configuration-based agents using YAML files, you can provide a directory path instead of a class name. The plugin will automatically use `ConfigAgentLoader` to scan for agent directories.
201+
202+
### Directory Structure
203+
204+
Create a parent directory containing subdirectories, each representing an agent with a `root_agent.yaml` file:
205+
206+
```
207+
my-agents/
208+
├── chat-assistant/
209+
│ └── root_agent.yaml
210+
├── search-agent/
211+
│ └── root_agent.yaml
212+
└── code-helper/
213+
├── root_agent.yaml
214+
└── another_agent.yaml
215+
```
216+
217+
### Usage with Directory Path
218+
219+
```bash
220+
mvn google-adk:web -Dagents=my-agents
221+
```
222+
223+
Or with absolute path:
224+
225+
```bash
226+
mvn google-adk:web -Dagents=/home/user/my-agents
227+
```
228+
229+
### Hot Reloading for Config Agents
230+
231+
When using config-based agents, hot reloading is enabled by default. The plugin will automatically detect changes to any YAML files within the agent directories and reload agents without restarting the server.
232+
233+
To disable hot reloading:
234+
235+
```bash
236+
mvn google-adk:web -Dagents=my-agents -DhotReloading=false
237+
```
238+
239+
### Example root_agent.yaml
240+
241+
```yaml
242+
name: "chat_assistant"
243+
description: "A friendly chat assistant"
244+
model: "gemini-2.0-flash"
245+
instruction: |
246+
You are a helpful and friendly assistant.
247+
Answer questions clearly and concisely.
248+
Be encouraging and positive in your responses.
249+
```
250+
198251
## Web UI
199252
200253
Once the server starts, open your browser to:
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.adk.maven;
18+
19+
import static java.util.stream.Collectors.toList;
20+
21+
import com.google.adk.agents.BaseAgent;
22+
import com.google.adk.agents.ConfigAgentUtils;
23+
import com.google.common.base.Suppliers;
24+
import com.google.common.collect.ImmutableList;
25+
import java.io.IOException;
26+
import java.nio.file.Files;
27+
import java.nio.file.Path;
28+
import java.nio.file.Paths;
29+
import java.util.Map;
30+
import java.util.NoSuchElementException;
31+
import java.util.concurrent.ConcurrentHashMap;
32+
import java.util.function.Supplier;
33+
import java.util.stream.Stream;
34+
import javax.annotation.Nonnull;
35+
import javax.annotation.concurrent.ThreadSafe;
36+
import org.slf4j.Logger;
37+
import org.slf4j.LoggerFactory;
38+
39+
/**
40+
* Configuration-based AgentLoader that loads agents from YAML configuration files.
41+
*
42+
* <p>This loader monitors a configured source directory for folders containing `root_agent.yaml`
43+
* files and automatically reloads agents when the files change (if hot-reloading is enabled).
44+
*
45+
* <p>The loader treats each subdirectory with a `root_agent.yaml` file as an agent, using the
46+
* folder name as the agent identifier. Agents are loaded lazily when first requested.
47+
*
48+
* <p>Directory structure expected:
49+
*
50+
* <pre>
51+
* source-dir/
52+
* ├── agent1/
53+
* │ └── root_agent.yaml
54+
* ├── agent2/
55+
* │ └── root_agent.yaml
56+
* └── ...
57+
* </pre>
58+
*
59+
* <p>Hot-reloading can be disabled by setting hotReloadingEnabled to false.
60+
*
61+
* <p>TODO: Config agent features are not yet ready for public use.
62+
*/
63+
@ThreadSafe
64+
class ConfigAgentLoader implements AgentLoader {
65+
private static final Logger logger = LoggerFactory.getLogger(ConfigAgentLoader.class);
66+
private static final String YAML_CONFIG_FILENAME = "root_agent.yaml";
67+
68+
private final boolean hotReloadingEnabled;
69+
private final String sourceDir;
70+
private final Map<String, Supplier<BaseAgent>> agentSuppliers = new ConcurrentHashMap<>();
71+
private final ConfigAgentWatcher watcher;
72+
private volatile boolean started = false;
73+
74+
/**
75+
* Creates a new ConfigAgentLoader.
76+
*
77+
* @param sourceDir The directory to scan for agent configuration files
78+
* @param hotReloadingEnabled Controls whether hot-reloading is enabled
79+
*/
80+
public ConfigAgentLoader(String sourceDir, boolean hotReloadingEnabled) {
81+
this.sourceDir = sourceDir;
82+
this.hotReloadingEnabled = hotReloadingEnabled;
83+
this.watcher = hotReloadingEnabled ? new ConfigAgentWatcher() : null;
84+
85+
try {
86+
discoverAgents();
87+
if (hotReloadingEnabled) {
88+
start();
89+
}
90+
} catch (IOException e) {
91+
logger.error("Failed to initialize ConfigAgentLoader", e);
92+
}
93+
}
94+
95+
/**
96+
* Creates a new ConfigAgentLoader with hot-reloading enabled.
97+
*
98+
* @param sourceDir The directory to scan for agent configuration files
99+
*/
100+
public ConfigAgentLoader(String sourceDir) {
101+
this(sourceDir, true);
102+
}
103+
104+
@Override
105+
@Nonnull
106+
public ImmutableList<String> listAgents() {
107+
return ImmutableList.copyOf(agentSuppliers.keySet());
108+
}
109+
110+
@Override
111+
public BaseAgent loadAgent(String name) {
112+
Supplier<BaseAgent> supplier = agentSuppliers.get(name);
113+
if (supplier == null) {
114+
throw new NoSuchElementException("Agent not found: " + name);
115+
}
116+
return supplier.get();
117+
}
118+
119+
/**
120+
* Discovers available agents from the configured source directory and creates suppliers for them.
121+
*
122+
* @throws IOException if there's an error accessing the source directory
123+
*/
124+
private void discoverAgents() throws IOException {
125+
if (sourceDir == null || sourceDir.isEmpty()) {
126+
logger.info(
127+
"Agent source directory not configured. ConfigAgentLoader will not discover any agents.");
128+
return;
129+
}
130+
131+
Path sourcePath = Paths.get(sourceDir);
132+
if (!Files.isDirectory(sourcePath)) {
133+
logger.warn(
134+
"Agent source directory does not exist: {}. ConfigAgentLoader will not discover any"
135+
+ " agents.",
136+
sourcePath);
137+
return;
138+
}
139+
140+
logger.info("Initial scan for YAML agents in: {}", sourcePath);
141+
142+
try (Stream<Path> entries = Files.list(sourcePath)) {
143+
for (Path agentDir : entries.collect(toList())) {
144+
if (Files.isDirectory(agentDir)) {
145+
Path yamlConfigPath = agentDir.resolve(YAML_CONFIG_FILENAME);
146+
if (Files.exists(yamlConfigPath) && Files.isRegularFile(yamlConfigPath)) {
147+
// Use the folder name as the agent identifier
148+
String agentName = agentDir.getFileName().toString();
149+
logger.debug("Discovering YAML agent config: {}", yamlConfigPath);
150+
151+
if (agentSuppliers.containsKey(agentName)) {
152+
logger.warn(
153+
"Duplicate agent name '{}' found in {}. Overwriting.", agentName, yamlConfigPath);
154+
}
155+
// Create a memoized supplier that will load the agent only when requested
156+
agentSuppliers.put(
157+
agentName, Suppliers.memoize(() -> loadAgentFromPath(yamlConfigPath)));
158+
159+
// Register with watcher if hot-reloading is enabled
160+
if (hotReloadingEnabled && watcher != null) {
161+
watcher.watch(agentDir, agentDirPath -> updateAgentSupplier(agentDirPath));
162+
}
163+
164+
logger.info("Discovered YAML agent '{}' from: {}", agentName, yamlConfigPath);
165+
}
166+
}
167+
}
168+
}
169+
170+
logger.info("Initial YAML agent discovery complete. Found {} agents.", agentSuppliers.size());
171+
}
172+
173+
/**
174+
* Updates the agent supplier when a configuration changes.
175+
*
176+
* @param agentDirPath The path to the agent configuration directory
177+
*/
178+
private void updateAgentSupplier(Path agentDirPath) {
179+
String agentName = agentDirPath.getFileName().toString();
180+
Path yamlConfigPath = agentDirPath.resolve(YAML_CONFIG_FILENAME);
181+
182+
if (Files.exists(yamlConfigPath)) {
183+
// File exists - create/update supplier
184+
agentSuppliers.put(agentName, Suppliers.memoize(() -> loadAgentFromPath(yamlConfigPath)));
185+
logger.info("Updated YAML agent supplier '{}' from: {}", agentName, yamlConfigPath);
186+
} else {
187+
// File deleted - remove supplier
188+
agentSuppliers.remove(agentName);
189+
logger.info("Removed YAML agent '{}' due to deleted config file", agentName);
190+
}
191+
}
192+
193+
/**
194+
* Loads an agent from the specified config path.
195+
*
196+
* @param yamlConfigPath The path to the YAML configuration file
197+
* @return The loaded BaseAgent
198+
* @throws RuntimeException if loading fails
199+
*/
200+
private BaseAgent loadAgentFromPath(Path yamlConfigPath) {
201+
try {
202+
logger.debug("Loading YAML agent from: {}", yamlConfigPath);
203+
BaseAgent agent = ConfigAgentUtils.fromConfig(yamlConfigPath.toString());
204+
logger.info("Successfully loaded YAML agent '{}' from: {}", agent.name(), yamlConfigPath);
205+
return agent;
206+
} catch (Exception e) {
207+
logger.error("Failed to load YAML agent from: {}", yamlConfigPath, e);
208+
throw new RuntimeException("Failed to load agent from: " + yamlConfigPath, e);
209+
}
210+
}
211+
212+
/**
213+
* Starts the hot-loading service. Sets up file watching.
214+
*
215+
* @throws IOException if there's an error accessing the source directory
216+
*/
217+
private synchronized void start() throws IOException {
218+
if (!hotReloadingEnabled || watcher == null) {
219+
logger.info(
220+
"Hot-reloading is disabled. YAML agents will be loaded once at startup and will not be"
221+
+ " monitored for changes.");
222+
return;
223+
}
224+
225+
if (started) {
226+
logger.warn("ConfigAgentLoader is already started");
227+
return;
228+
}
229+
230+
logger.info("Starting ConfigAgentLoader with file watching");
231+
watcher.start();
232+
started = true;
233+
logger.info("ConfigAgentLoader started successfully with {} agents.", agentSuppliers.size());
234+
}
235+
236+
/** Stops the hot-loading service. */
237+
public synchronized void stop() {
238+
if (!started) {
239+
return;
240+
}
241+
242+
logger.info("Stopping ConfigAgentLoader...");
243+
if (watcher != null) {
244+
watcher.stop();
245+
}
246+
started = false;
247+
logger.info("ConfigAgentLoader stopped.");
248+
}
249+
}

0 commit comments

Comments
 (0)