Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ contains more complete end-to-end samples for customers to use or modify.
prime-checking tools.
- [`mcpfilesystem`](mcpfilesystem/README.md): an agent that uses the MCP
filesystem server as a toolset.
- [`spring_chat`](spring_chat/README.md): a Spring Boot chat page backed by an
agent that calls its model through Spring AI.
1 change: 1 addition & 0 deletions examples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@
<module>contextcaching</module>
<module>helloworld</module>
<module>mcpfilesystem</module>
<module>spring_chat</module>
</modules>
</project>
46 changes: 46 additions & 0 deletions examples/spring_chat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Spring Chat Sample

A minimal Spring Boot web app with a browser chat window backed by an ADK agent.
The agent calls its model through Spring AI, using the ADK Spring AI integration
(`google-adk-spring-ai`).

## How it fits together

- `spring-ai-starter-model-google-genai` creates a Spring AI `ChatModel`.
- The ADK Spring AI auto-configuration wraps it in a `SpringAI` bean, which is
an ADK `BaseLlm`.
- `AgentConfig` builds an `LlmAgent` on that model and an `InMemoryRunner`.
- `ChatController` exposes `POST /api/chat`, and `static/index.html` is the
chat page. Each page load gets its own ADK session, so the agent remembers
the conversation until you reload. Sessions are kept in memory until the app
stops.

To use another provider, replace the Google GenAI starter in `pom.xml` with any
other Spring AI chat model starter (OpenAI, Anthropic, Ollama, ...) and set its
`spring.ai.*` properties.

## Prerequisites

- Java 17+
- A Google Cloud project with Vertex AI enabled, and Application Default
Credentials (`gcloud auth application-default login`).

The app uses Vertex AI by default. Set `GOOGLE_CLOUD_PROJECT`, and optionally
`GOOGLE_CLOUD_LOCATION` (default `global`) and `GEMINI_MODEL`. To use the Gemini
Developer API instead, set `GOOGLE_GENAI_USE_VERTEXAI=false` and
`GOOGLE_API_KEY`.

## Run

From the repository root:

```bash
export GOOGLE_CLOUD_PROJECT=your-project-id
./mvnw install -DskipTests -pl examples/spring_chat -am
./mvnw spring-boot:run -pl examples/spring_chat
```

Then open http://localhost:8080. The chat endpoint has no authentication, so the
app listens on `localhost` only, which keeps other machines out. Set
`SERVER_ADDRESS=0.0.0.0` to accept connections from other hosts. Don't expose it
beyond a trusted environment without adding authentication.
91 changes: 91 additions & 0 deletions examples/spring_chat/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.google.adk</groupId>
<artifactId>google-adk-examples</artifactId>
<version>1.13.0</version><!-- {x-version-update:google-adk:current} -->
<relativePath>..</relativePath>
</parent>

<groupId>com.google.adk.samples</groupId>
<artifactId>google-adk-sample-spring-chat</artifactId>
<name>Google ADK - Sample - Spring Chat</name>
<description>
A Spring Boot web app with a chat page backed by an ADK agent that calls its model through
Spring AI.
</description>
<packaging>jar</packaging>

<properties>
<spring-ai.version>2.0.1</spring-ai.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>com.google.adk</groupId>
<artifactId>google-adk</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.google.adk</groupId>
<artifactId>google-adk-spring-ai</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<!-- Any Spring AI chat model starter works; swap this one to change providers. -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-google-genai</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
<plugin>
<!-- Like the contextcaching demo, this example is not released to Maven. -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.springchat;

import com.google.adk.agents.BaseAgent;
import com.google.adk.agents.LlmAgent;
import com.google.adk.models.springai.SpringAI;
import com.google.adk.runner.InMemoryRunner;
import com.google.adk.runner.Runner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/** Spring configuration for the ADK agent and runner. */
@Configuration(proxyBeanMethods = false)
class AgentConfig {

private static final String APP_NAME = "spring-chat";

/**
* Returns the chat agent. Its {@link SpringAI} model is auto-configured by the ADK Spring AI
* integration from whichever Spring AI {@code ChatModel} starter is on the classpath.
*/
@Bean
BaseAgent chatAgent(SpringAI model) {
return LlmAgent.builder()
.name("spring_chat_agent")
.description("A friendly general-purpose chat assistant.")
.model(model)
.instruction("You are a friendly, concise assistant. Answer the user's questions clearly.")
.build();
}

@Bean
Runner runner(BaseAgent chatAgent) {
return new InMemoryRunner(chatAgent, APP_NAME);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.springchat;

import com.google.adk.agents.RunConfig;
import com.google.adk.events.Event;
import com.google.adk.runner.Runner;
import com.google.genai.types.Content;
import com.google.genai.types.Part;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

/** REST endpoint that forwards chat messages from the browser to the ADK runner. */
@RestController
class ChatController {

// The sample has no login, so every browser tab shares one user and keeps its own session.
private static final String USER_ID = "spring-chat-user";

private static final RunConfig RUN_CONFIG = RunConfig.builder().autoCreateSession(true).build();

private final Runner runner;

ChatController(Runner runner) {
this.runner = runner;
}

/** A message from the browser; {@code sessionId} is generated client-side once per page load. */
private record ChatRequest(@Nullable String sessionId, @Nullable String message) {}

private record ChatResponse(String reply) {}

@PostMapping("/api/chat")
ChatResponse chat(@RequestBody ChatRequest request) {
if (isBlank(request.sessionId()) || isBlank(request.message())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
// The session service trims ids it creates, so look them up trimmed too.
String sessionId = request.sessionId().trim();
Content userMessage = Content.fromParts(Part.fromText(request.message()));
String reply =
runner
.runAsync(USER_ID, sessionId, userMessage, RUN_CONFIG)
.filter(Event::finalResponse)
.map(ChatController::replyText)
.collect(Collectors.joining("\n"))
.blockingGet();
return new ChatResponse(reply.strip());
}

/** Returns the event's text, followed by why the model stopped, if it ended early. */
private static String replyText(Event event) {
String text = event.stringifyContent();
return event.errorMessage().map(error -> text + "\n[" + error + "]").orElse(text);
}

private static boolean isBlank(@Nullable String s) {
return s == null || s.trim().isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.springchat;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/** Spring Boot entry point for the chat sample. */
@SpringBootApplication
public class SpringChatApplication {

public static void main(String[] args) {
SpringApplication.run(SpringChatApplication.class, args);
}
}
11 changes: 11 additions & 0 deletions examples/spring_chat/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Spring AI Google GenAI chat model. Vertex AI with Application Default Credentials is the default:
# set GOOGLE_CLOUD_PROJECT (and optionally GOOGLE_CLOUD_LOCATION). To use the Gemini Developer API
# instead, set GOOGLE_GENAI_USE_VERTEXAI=false and GOOGLE_API_KEY.
spring.ai.google.genai.vertex-ai=${GOOGLE_GENAI_USE_VERTEXAI:true}
spring.ai.google.genai.project-id=${GOOGLE_CLOUD_PROJECT:}
spring.ai.google.genai.location=${GOOGLE_CLOUD_LOCATION:global}
spring.ai.google.genai.api-key=${GOOGLE_API_KEY:}
spring.ai.google.genai.chat.model=${GEMINI_MODEL:gemini-3.8-flash}

# The chat endpoint has no authentication, so listen on loopback only unless told otherwise.
server.address=${SERVER_ADDRESS:localhost}
Loading
Loading