From bca05915b244cfc23bfd738c4feadc2ae5f53f58 Mon Sep 17 00:00:00 2001 From: Rafal Glowinski Date: Thu, 29 Oct 2026 04:35:33 -0700 Subject: [PATCH] feat: add a Spring Boot chat app backed by an ADK agent PiperOrigin-RevId: 1009593322 --- examples/README.md | 2 + examples/pom.xml | 1 + examples/spring_chat/README.md | 46 ++++++++++ examples/spring_chat/pom.xml | 91 +++++++++++++++++++ .../com/example/springchat/AgentConfig.java | 51 +++++++++++ .../example/springchat/ChatController.java | 79 ++++++++++++++++ .../springchat/SpringChatApplication.java | 29 ++++++ .../src/main/resources/application.properties | 11 +++ .../src/main/resources/static/index.html | 82 +++++++++++++++++ 9 files changed, 392 insertions(+) create mode 100644 examples/spring_chat/README.md create mode 100644 examples/spring_chat/pom.xml create mode 100644 examples/spring_chat/src/main/java/com/example/springchat/AgentConfig.java create mode 100644 examples/spring_chat/src/main/java/com/example/springchat/ChatController.java create mode 100644 examples/spring_chat/src/main/java/com/example/springchat/SpringChatApplication.java create mode 100644 examples/spring_chat/src/main/resources/application.properties create mode 100644 examples/spring_chat/src/main/resources/static/index.html diff --git a/examples/README.md b/examples/README.md index 65768a1b9..d7304d1eb 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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. diff --git a/examples/pom.xml b/examples/pom.xml index b52d70446..8df3003ef 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -37,5 +37,6 @@ contextcaching helloworld mcpfilesystem + spring_chat diff --git a/examples/spring_chat/README.md b/examples/spring_chat/README.md new file mode 100644 index 000000000..f60236ed0 --- /dev/null +++ b/examples/spring_chat/README.md @@ -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. diff --git a/examples/spring_chat/pom.xml b/examples/spring_chat/pom.xml new file mode 100644 index 000000000..b2e259703 --- /dev/null +++ b/examples/spring_chat/pom.xml @@ -0,0 +1,91 @@ + + + + 4.0.0 + + + com.google.adk + google-adk-examples + 1.13.0 + .. + + + com.google.adk.samples + google-adk-sample-spring-chat + Google ADK - Sample - Spring Chat + + A Spring Boot web app with a chat page backed by an ADK agent that calls its model through + Spring AI. + + jar + + + 2.0.1 + + + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + + + + + com.google.adk + google-adk + ${project.version} + + + com.google.adk + google-adk-spring-ai + ${project.version} + + + org.springframework.boot + spring-boot-starter-webmvc + + + + org.springframework.ai + spring-ai-starter-model-google-genai + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + diff --git a/examples/spring_chat/src/main/java/com/example/springchat/AgentConfig.java b/examples/spring_chat/src/main/java/com/example/springchat/AgentConfig.java new file mode 100644 index 000000000..374f4d7a7 --- /dev/null +++ b/examples/spring_chat/src/main/java/com/example/springchat/AgentConfig.java @@ -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); + } +} diff --git a/examples/spring_chat/src/main/java/com/example/springchat/ChatController.java b/examples/spring_chat/src/main/java/com/example/springchat/ChatController.java new file mode 100644 index 000000000..f1272a0d2 --- /dev/null +++ b/examples/spring_chat/src/main/java/com/example/springchat/ChatController.java @@ -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(); + } +} diff --git a/examples/spring_chat/src/main/java/com/example/springchat/SpringChatApplication.java b/examples/spring_chat/src/main/java/com/example/springchat/SpringChatApplication.java new file mode 100644 index 000000000..c0bbfaf82 --- /dev/null +++ b/examples/spring_chat/src/main/java/com/example/springchat/SpringChatApplication.java @@ -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); + } +} diff --git a/examples/spring_chat/src/main/resources/application.properties b/examples/spring_chat/src/main/resources/application.properties new file mode 100644 index 000000000..5136a5856 --- /dev/null +++ b/examples/spring_chat/src/main/resources/application.properties @@ -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} diff --git a/examples/spring_chat/src/main/resources/static/index.html b/examples/spring_chat/src/main/resources/static/index.html new file mode 100644 index 000000000..50671484d --- /dev/null +++ b/examples/spring_chat/src/main/resources/static/index.html @@ -0,0 +1,82 @@ + + + + + + ADK Spring Chat + + + +

ADK Spring Chat

+
+
+ + +
+ + + +