Skip to content

Commit 87acdf8

Browse files
Doris26copybara-github
authored andcommitted
fix: Fixed AgentStaticLoader bean registration using ApplicationContextInitializer and resolved OpenTelemetry double initialization in tests
This CL fixes a bug where agents provided to AdkWebServer.start() were not used. The previous implementation set a static field that Spring's dependency injection ignored, always using CompiledAgentLoader. The fix involves programmatically registering an AgentStaticLoader instance as a bean within the Spring context using an ApplicationContextInitializer, ensuring it's correctly injected. The bean name for the loader has been standardized to "agentProvider", and tutorial documentation is updated to reflect the changes. PiperOrigin-RevId: 800576785
1 parent 4ca52b7 commit 87acdf8

File tree

5 files changed

+100
-49
lines changed

5 files changed

+100
-49
lines changed

dev/src/main/java/com/google/adk/web/AdkWebServer.java

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,13 @@
8181
import org.slf4j.Logger;
8282
import org.slf4j.LoggerFactory;
8383
import org.springframework.beans.factory.annotation.Autowired;
84-
import org.springframework.beans.factory.annotation.Qualifier;
8584
import org.springframework.beans.factory.annotation.Value;
85+
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
8686
import org.springframework.boot.SpringApplication;
8787
import org.springframework.boot.autoconfigure.SpringBootApplication;
8888
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
89+
import org.springframework.context.ApplicationContextInitializer;
90+
import org.springframework.context.ConfigurableApplicationContext;
8991
import org.springframework.context.annotation.Bean;
9092
import org.springframework.context.annotation.Configuration;
9193
import org.springframework.http.HttpStatus;
@@ -124,9 +126,6 @@ public class AdkWebServer implements WebMvcConfigurer {
124126

125127
private static final Logger log = LoggerFactory.getLogger(AdkWebServer.class);
126128

127-
// Static agent loader for programmatic startup
128-
private static AgentLoader AGENT_LOADER;
129-
130129
// WebSocket constants
131130
private static final String LIVE_REQUEST_QUEUE_ATTR = "liveRequestQueue";
132131
private static final String LIVE_SUBSCRIPTION_ATTR = "liveSubscription";
@@ -195,7 +194,7 @@ public static class RunnerService {
195194

196195
@Autowired
197196
public RunnerService(
198-
@Qualifier("agentLoader") AgentLoader agentProvider,
197+
AgentLoader agentProvider,
199198
BaseArtifactService artifactService,
200199
BaseSessionService sessionService,
201200
BaseMemoryService memoryService) {
@@ -278,11 +277,24 @@ public SdkTracerProvider sdkTracerProvider(ApiServerSpanExporter apiServerSpanEx
278277
@Bean
279278
public OpenTelemetry openTelemetrySdk(SdkTracerProvider sdkTracerProvider) {
280279
otelLog.debug("Configuring OpenTelemetrySdk and registering globally.");
281-
OpenTelemetrySdk otelSdk =
282-
OpenTelemetrySdk.builder().setTracerProvider(sdkTracerProvider).buildAndRegisterGlobal();
283280

284-
Runtime.getRuntime().addShutdownHook(new Thread(otelSdk::close));
285-
return otelSdk;
281+
// Check if OpenTelemetry has already been set globally (common in tests)
282+
try {
283+
io.opentelemetry.api.GlobalOpenTelemetry.get();
284+
// If we get here, it's already set, so just return a new instance without global
285+
// registration
286+
otelLog.debug("OpenTelemetry already registered globally, creating non-global instance.");
287+
return OpenTelemetrySdk.builder().setTracerProvider(sdkTracerProvider).build();
288+
} catch (IllegalStateException e) {
289+
// GlobalOpenTelemetry hasn't been set yet, safe to register globally
290+
otelLog.debug("Registering OpenTelemetry globally.");
291+
OpenTelemetrySdk otelSdk =
292+
OpenTelemetrySdk.builder()
293+
.setTracerProvider(sdkTracerProvider)
294+
.buildAndRegisterGlobal();
295+
Runtime.getRuntime().addShutdownHook(new Thread(otelSdk::close));
296+
return otelSdk;
297+
}
286298
}
287299
}
288300

@@ -623,15 +635,15 @@ public static class AgentController {
623635
public AgentController(
624636
BaseSessionService sessionService,
625637
BaseArtifactService artifactService,
626-
@Qualifier("agentLoader") AgentLoader agentProvider,
638+
AgentLoader agentProvider,
627639
ApiServerSpanExporter apiServerSpanExporter,
628640
RunnerService runnerService) {
629641
this.sessionService = sessionService;
630642
this.artifactService = artifactService;
631643
this.agentProvider = agentProvider;
632644
this.apiServerSpanExporter = apiServerSpanExporter;
633645
this.runnerService = runnerService;
634-
ImmutableList<String> agentNames = agentProvider.listAgents();
646+
ImmutableList<String> agentNames = this.agentProvider.listAgents();
635647
log.info(
636648
"AgentController initialized with {} dynamic agents: {}", agentNames.size(), agentNames);
637649
if (agentNames.isEmpty()) {
@@ -1862,11 +1874,23 @@ public static void main(String[] args) {
18621874
}
18631875

18641876
// TODO(vorburger): #later return Closeable, which can stop the server (and resets static)
1865-
public static synchronized void start(BaseAgent... agents) {
1866-
if (AGENT_LOADER != null) {
1867-
throw new IllegalStateException("AdkWebServer can only be started once.");
1868-
}
1869-
AGENT_LOADER = new AgentStaticLoader(agents);
1870-
main(new String[0]);
1877+
public static void start(BaseAgent... agents) {
1878+
// Disable CompiledAgentLoader by setting property to prevent its creation
1879+
System.setProperty("adk.agents.loader", "static");
1880+
1881+
// Create Spring Application with custom initializer
1882+
SpringApplication app = new SpringApplication(AdkWebServer.class);
1883+
app.addInitializers(
1884+
new ApplicationContextInitializer<ConfigurableApplicationContext>() {
1885+
@Override
1886+
public void initialize(ConfigurableApplicationContext context) {
1887+
// Register the AgentStaticLoader bean before context refresh
1888+
DefaultListableBeanFactory beanFactory =
1889+
(DefaultListableBeanFactory) context.getBeanFactory();
1890+
beanFactory.registerSingleton("agentLoader", new AgentStaticLoader(agents));
1891+
}
1892+
});
1893+
1894+
app.run(new String[0]);
18711895
}
18721896
}

dev/src/main/java/com/google/adk/web/AgentStaticLoader.java

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@
2626
import com.google.common.collect.ImmutableMap;
2727
import java.util.NoSuchElementException;
2828
import javax.annotation.Nonnull;
29-
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
30-
import org.springframework.stereotype.Service;
3129

3230
/**
3331
* Static Agent Loader for programmatically provided agents.
@@ -36,23 +34,10 @@
3634
* through the AgentLoader interface. Perfect for cases where you already have agent instances and
3735
* just need a convenient way to wrap them in an AgentLoader.
3836
*
39-
* <p>Example usage:
40-
*
41-
* <pre>
42-
* List&lt;BaseAgent&gt; agents = Arrays.asList(new MyAgent(), new CodeAssistant());
43-
* AgentLoader loader = new AgentStaticLoader(agents.toArray(new BaseAgent[0]));
44-
* app.beanFactory().setAgentLoader(loader);
45-
* </pre>
46-
*
47-
* <p>Configuration:
48-
*
49-
* <ul>
50-
* <li>To enable this loader: {@code adk.agents.loader=static}
51-
* </ul>
37+
* <p>This class is not a Spring component by itself - instances are created programmatically and
38+
* then registered as beans via factory methods.
5239
*/
53-
@Service("staticAgentLoader")
54-
@ConditionalOnProperty(name = "adk.agents.loader", havingValue = "static", matchIfMissing = false)
55-
public class AgentStaticLoader implements AgentLoader {
40+
class AgentStaticLoader implements AgentLoader {
5641

5742
private final ImmutableMap<String, BaseAgent> agents;
5843

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.google.adk.web;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import com.google.adk.agents.BaseAgent;
7+
import com.google.adk.agents.LlmAgent;
8+
import org.junit.jupiter.api.Test;
9+
10+
public class AgentStaticLoaderTest {
11+
12+
@Test
13+
public void testAgentStaticLoaderApproach() {
14+
BaseAgent testAgent =
15+
LlmAgent.builder()
16+
.name("test_agent")
17+
.model("gemini-2.0-flash-lite")
18+
.description("Test agent for demonstrating AgentStaticLoader")
19+
.instruction("You are a test agent.")
20+
.build();
21+
22+
AgentStaticLoader staticLoader = new AgentStaticLoader(testAgent);
23+
24+
assertTrue(staticLoader.listAgents().contains("test_agent"));
25+
assertEquals(testAgent, staticLoader.loadAgent("test_agent"));
26+
assertEquals("test_agent", staticLoader.loadAgent("test_agent").name());
27+
}
28+
}

tutorials/city-time-weather/README.md

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,34 @@ cd /google_adk/tutorials/city-time-weather
1616

1717
## Running the Agent
1818

19-
This tutorial supports multiple ways to load and run the agent:
19+
This tutorial demonstrates two different agent loading approaches:
2020

21-
### Option 1: Using CompiledAgentLoader (Recommended)
21+
### Option 1: Using CompiledAgentLoader (Automatic Discovery)
2222

23-
The simplest approach - automatically discovers agents with `ROOT_AGENT` fields:
23+
Automatically discovers agents with `ROOT_AGENT` fields in compiled classes:
2424

2525
```shell
2626
mvn exec:java -Dadk.agents.source-dir=$PWD
2727
```
2828

29-
### Option 2: Using AgentStaticLoader Programmatically
29+
This approach:
30+
- Uses Spring Boot to start `AdkWebServer` directly
31+
- `CompiledAgentLoader` scans `target/classes` for `ROOT_AGENT` fields
32+
- Automatically loads the `CityTimeWeather.ROOT_AGENT`
3033

31-
For programmatic control, use the `AdkWebServer.start()` method:
34+
### Option 2: Using AgentStaticLoader (Programmatic)
3235

36+
Explicitly provides pre-created agent instances:
37+
38+
With custom port:
3339
```shell
34-
mvn exec:java -Dexec.mainClass="com.google.adk.tutorials.CityTimeWeather"
40+
mvn exec:java -Dexec.mainClass="com.google.adk.tutorials.CityTimeWeather" -Dserver.port=8081
3541
```
3642

37-
This uses the built-in `main` method that calls:
38-
```java
39-
AdkWebServer.start(ROOT_AGENT);
40-
```
43+
This approach:
44+
- Calls `CityTimeWeather.main()` which executes `AdkWebServer.start(ROOT_AGENT)`
45+
- Directly provides the agent instance programmatically
46+
- Uses `AgentStaticLoader` with the provided agent
4147

4248
## Usage
4349

@@ -46,10 +52,13 @@ Once running, you can interact with the agent through:
4652
- API endpoints for city time and weather queries
4753
- Agent name: `multi_tool_agent`
4854

49-
## Agent Loaders Explained
55+
## Agent Loading Approaches
56+
57+
This tutorial demonstrates both agent loading strategies:
58+
59+
- **CompiledAgentLoader (Option 1)**: Automatically discovers agents in compiled classes. Good for development and when you have multiple agents.
60+
- **AgentStaticLoader (Option 2)**: Takes pre-created agent instances programmatically. Good for production and when you need precise control.
5061

51-
- **CompiledAgentLoader**: Scans directories for compiled classes with `ROOT_AGENT` fields
52-
- **AgentStaticLoader**: Takes pre-created agent instances programmatically
53-
- Choose based on whether you want automatic discovery or programmatic control
62+
Choose Option 1 for automatic discovery, Option 2 for programmatic control.
5463

5564
See https://google.github.io/adk-docs/get-started/quickstart/#java for more information.

tutorials/city-time-weather/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,10 @@
4444
</exclusion>
4545
</exclusions>
4646
</dependency>
47+
<dependency>
48+
<groupId>org.slf4j</groupId>
49+
<artifactId>slf4j-simple</artifactId>
50+
<version>1.7.36</version>
51+
</dependency>
4752
</dependencies>
4853
</project>

0 commit comments

Comments
 (0)