# ACP Java SDK Source: https://springaicommunity.mintlify.app/acp-java-sdk/index A Java SDK for the Agent Client Protocol — build both clients and agents that work with Zed, JetBrains, VS Code, and any ACP-compliant editor. Current documentation for ACP Java SDK is at [lab.pollack.ai/projects/acp-java-sdk](https://lab.pollack.ai/projects/acp-java-sdk). The content below may be outdated. A pure Java implementation of the [Agent Client Protocol (ACP)](https://agentclientprotocol.com/) specification. Build clients that connect to ACP agents, or build agents that run inside code editors. ## How ACP Works ACP uses a subprocess model. A **client** (your application, or an editor like Zed) launches an **agent** as a child process and communicates over stdin/stdout using JSON-RPC messages. The protocol has three phases: 1. **Initialize** — client and agent exchange protocol versions and capabilities 2. **Session** — client creates a session with a working directory context 3. **Prompt** — client sends messages, agent streams back responses This is the same mechanism that Zed, JetBrains, and VS Code use to talk to AI agents. The SDK lets you build either side of that conversation. ## Overview The ACP Java SDK provides: * **Client SDK** — connect to and interact with any ACP-compliant agent * **Agent SDK** — build ACP-compliant agents that work in Zed, JetBrains, and VS Code * **Test utilities** — in-memory transports for fast, deterministic testing ## Quick Start ### Try it now The fastest way to see ACP in action is to clone the tutorial and run a module. The client example talks to [Gemini CLI](https://github.com/google-gemini/gemini-cli) (requires `GEMINI_API_KEY`). The agent example runs locally with no API key. ```bash theme={null} git clone https://github.com/markpollack/acp-java-tutorial.git cd acp-java-tutorial # Client: talk to Gemini as an ACP agent export GEMINI_API_KEY=your-key-here ./mvnw exec:java -pl module-01-first-contact # Agent: build and run your own (no API key needed) ./mvnw package -pl module-12-echo-agent -q ./mvnw exec:java -pl module-12-echo-agent ``` ### Client — Connect to an Agent This example launches [Gemini CLI](https://github.com/google-gemini/gemini-cli) as an ACP agent subprocess and sends it a prompt. Any CLI tool that speaks ACP over stdin/stdout works here — Gemini CLI is one such tool. `AgentParameters` builds the command line (`gemini --experimental-acp`). `StdioAcpClientTransport` spawns the process and handles the JSON-RPC framing. The `sessionUpdateConsumer` receives the agent's response text as it streams in — without it, you'd get the stop reason but no visible output. ```java theme={null} import com.agentclientprotocol.sdk.client.*; import com.agentclientprotocol.sdk.client.transport.*; import com.agentclientprotocol.sdk.spec.AcpSchema.*; import java.util.List; // Launch Gemini CLI as an ACP agent subprocess var params = AgentParameters.builder("gemini").arg("--experimental-acp").build(); var transport = new StdioAcpClientTransport(params); AcpSyncClient client = AcpClient.sync(transport) .sessionUpdateConsumer(notification -> { // Print the agent's response as it streams in if (notification.update() instanceof AgentMessageChunk msg) { System.out.print(((TextContent) msg.content()).text()); } }) .build(); client.initialize(); var session = client.newSession(new NewSessionRequest(".", List.of())); var response = client.prompt(new PromptRequest( session.sessionId(), List.of(new TextContent("What is 2+2? Reply with just the number.")) )); System.out.println("\nStop reason: " + response.stopReason()); client.close(); // Output: 4 // Stop reason: END_TURN ``` Run this yourself: [Module 01: First Contact](https://github.com/markpollack/acp-java-tutorial/tree/main/module-01-first-contact) — full source with error handling and setup. ### Agent — Build Your Own This is the other side of the conversation. When you build an agent, **editors and clients launch your code** as a subprocess and send it JSON-RPC messages over stdin. Your agent handles three request types: initialize, new session, and prompt. The stdio transport reads from stdin and writes to stdout. `run()` blocks until the client disconnects. The module includes a demo client that launches this agent as a subprocess and exercises it — you'll see `Echo: Hello!` printed when you run it. ```java theme={null} import com.agentclientprotocol.sdk.annotation.*; import com.agentclientprotocol.sdk.agent.SyncPromptContext; import com.agentclientprotocol.sdk.agent.support.AcpAgentSupport; import com.agentclientprotocol.sdk.spec.AcpSchema.*; @AcpAgent class EchoAgent { @Initialize InitializeResponse init() { return InitializeResponse.ok(); } @NewSession NewSessionResponse newSession() { return new NewSessionResponse(UUID.randomUUID().toString(), null, null); } @Prompt PromptResponse prompt(PromptRequest req, SyncPromptContext ctx) { ctx.sendMessage("Echo: " + req.text()); return PromptResponse.endTurn(); } } // Reads JSON-RPC from stdin, writes to stdout — editors connect here AcpAgentSupport.create(new EchoAgent()) .transport(new StdioAcpAgentTransport()) .run(); ``` Run this yourself: [Module 12: Echo Agent](https://github.com/markpollack/acp-java-tutorial/tree/main/module-12-echo-agent) — includes a demo client that launches the agent and sends test prompts. No API key required. ### Adding the SDK to your project ```xml theme={null} com.agentclientprotocol acp-core 0.11.0 ``` For annotation-based agents (as shown above), add `acp-agent-support` instead — it includes `acp-core` transitively: ```xml theme={null} com.agentclientprotocol acp-agent-support 0.11.0 ``` ## Three Agent API Styles | Style | Entry Point | Best For | | -------------------- | ---------------------- | --------------------------------------------- | | **Annotation-based** | `@AcpAgent`, `@Prompt` | Least boilerplate, declarative style | | **Sync** | `AcpAgent.sync()` | Blocking handlers, plain return values | | **Async** | `AcpAgent.async()` | Reactive applications, Project Reactor `Mono` | All three styles produce identical protocol behavior. Choose based on programming preference. ## Documentation Client API, Agent API (all three styles), protocol types, transports, errors 30-module progressive tutorial from client basics to IDE integration ## Resources * [GitHub Repository](https://github.com/agentclientprotocol/java-sdk) — Source code * [ACP Java Tutorial](https://github.com/markpollack/acp-java-tutorial) — 30 hands-on modules ### ACP Ecosystem * [Agent Client Protocol](https://agentclientprotocol.com/) — Official specification * [ACP Specification](https://agentclientprotocol.com/protocol/overview) — Protocol details (initialization, sessions, prompt turns) * [Agents Directory](https://agentclientprotocol.com/overview/agents) — ACP-compliant agents * [Clients Directory](https://agentclientprotocol.com/overview/clients) — Editors and clients that support ACP ### Other ACP SDKs * [Kotlin SDK](https://github.com/agentclientprotocol/kotlin-sdk) * [Python SDK](https://github.com/agentclientprotocol/python-sdk) * [TypeScript SDK](https://github.com/agentclientprotocol/typescript-sdk) * [Rust SDK](https://github.com/agentclientprotocol/rust-sdk) ### Editor Documentation * [Zed — External Agents](https://zed.dev/docs/ai/external-agents) * [JetBrains — ACP Support](https://www.jetbrains.com/help/ai-assistant/acp.html) * [VS Code — ACP Extension](https://github.com/formulahendry/vscode-acp) # AI Meets Spring Petclinic: Implementing an AI Assistant with Spring AI (Part I) Source: https://springaicommunity.mintlify.app/blog/advisors/petclinic-part-1 In this two-parts blog post, I will discuss the modifications I made to [Spring Petclinic](https://github.com/spring-projects/spring-petclinic/tree/spring-ai)... ## Introduction In this two-parts blog post, I will discuss the modifications I made to [Spring Petclinic](https://github.com/spring-projects/spring-petclinic/tree/spring-ai) to incorporate an AI assistant that allows users to interact with the application using natural language. ## Introduction to Spring Petclinic Spring Petclinic serves as the primary reference application within the Spring ecosystem. According to GitHub, the repository was created on January 9, 2013. Since then, it has become the model application for writing simple, developer-friendly code using Spring Boot. As of this writing, it has garnered over 7,600 stars and 23,000 forks. ![e386a28e-e860-4cf7-a94c-aa7dad13abe3](https://static.spring.io/blog/contentful/20240923/e386a28e-e860-4cf7-a94c-aa7dad13abe3.png) The application simulates a management system for a veterinarian’s pet clinic. Within the application, users can perform several activities: * List pet owners * Add a new owner * Add a pet to an owner * Document a visit for a specific pet * List the veterinarians in the clinic * Simulate a server-side error While the application is simple and straightforward, it effectively showcases the ease of use associated with developing Spring Boot applications. Additionally, the Spring team has continuously updated the app to support the latest versions of the Spring Framework and Spring Boot. ## Technologies Used Spring Petclinic is developed using Spring Boot, specifically version 3.3 as of this publication. ### Frontend UI The frontend UI is built using [Thymeleaf](https://www.thymeleaf.org/). Thymeleaf's templating engine facilitates seamless backend API calls within the HTML code, making it easy to understand. Below is the code that retrieves the list of pet owners: ```xml theme={null}
Name Specialties
none
``` The key line here is `${listVets}`, which references a model in the Spring backend that contains the data to be populated. Below is the relevant code block from the Spring `@Controller` that populates this model: ```java theme={null} private String addPaginationModel(int page, Page paginated, Model model) { List listVets = paginated.getContent(); model.addAttribute("currentPage", page); model.addAttribute("totalPages", paginated.getTotalPages()); model.addAttribute("totalItems", paginated.getTotalElements()); model.addAttribute("listVets", listVets); return "vets/vetList"; } ``` ### Spring Data JPA Petclinic interacts with the database using the Java Persistence API (JPA). It supports H2, PostgreSQL, or MySQL, depending on the selected profile. Database communication is facilitated through `@Repository` interfaces, such as `OwnerRepository`. Here’s an example of one of the JPA queries within the interface: ```java theme={null} /** * Returns all the owners from data store **/ @Query("SELECT owner FROM Owner owner") @Transactional(readOnly = true) Page findAll(Pageable pageable); ``` JPA significantly simplifies your code by automatically implementing default queries for your methods based on naming conventions. It also allows you to specify a JPQL query using the `@Query` annotation when needed. ## Hello, Spring AI Spring AI is one of the most exciting new projects in the Spring ecosystem in recent memory. It enables interaction with popular large language models (LLMs) using familiar Spring paradigms and techniques. Much like Spring Data provides an abstraction that allows you to write your code once, delegating implementation to the provided `spring-boot-starter` dependency and property configuration, Spring AI offers a similar approach for LLMs. You write your code once to an interface, and a `@Bean` is injected at runtime for your specific implementation. Spring AI supports all the major large language models, including OpenAI, Azure’s OpenAI implementation, Google Gemini, Amazon Bedrock, and [many more](https://docs.spring.io/spring-ai/reference/api/chatmodel.html). ## Considerations for implementing AI in Spring Petclinic Spring Petclinic is over 10 years old and was not originally designed with AI in mind. It serves as a classic candidate for testing the integration of AI into a “legacy” codebase. In approaching the challenge of adding an AI assistant to Spring Petclinic, I had to consider several important factors. ### **Selecting a Model API** The first consideration was determining the type of API I wanted to implement. Spring AI offers a variety of capabilities, including support for chat, image recognition and generation, audio transcription, text-to-speech, and more. For Spring Petclinic, a familiar “chatbot” interface made the most sense. This would allow clinic employees to communicate with the system in natural language, streamlining their interactions instead of navigating through UI tabs and forms. I would also need embedding capabilities, which will be used for Retrieval-Augmented Generation (RAG) later in the article. ![70d36ac5-3e2a-4dae-9ab7-94cecaf4f493](https://static.spring.io/blog/contentful/20240923/70d36ac5-3e2a-4dae-9ab7-94cecaf4f493.png) Possible interactions with the AI assistant may include: * How can you assist me? * Please list the owners that come to our clinic. * Which vets specialize in radiology? * Is there a pet owner named Betty? * Which of the owners have dogs? * Add a dog for Betty; its name is Moopsie. These examples illustrate the range of queries the AI can handle. The strength of LLMs lies in their ability to comprehend natural language and provide meaningful responses. ### Selecting a Large Language Model provider The tech world is currently experiencing a gold rush with large language models (LLMs), with new models emerging every few days, each offering enhanced capabilities, larger context windows, and advanced features such as improved reasoning. Some of the popular LLMs include: * OpenAI and its Azure-based service, Azure OpenAI * Google Gemini * Amazon Bedrock, a managed AWS service that can run various LLMs, including Anthropic and Titan * Llama 3.1, along with many other open-source LLMs available through [Hugging Face](https://huggingface.co/) For our Petclinic application, I required a model that excels in chat capabilities, can be tailored to my application’s specific needs, and supports function calling (more on that later!). One of the great advantages of Spring AI is the ease of conducting A/B testing with various LLMs. You simply change a dependency and update a few properties. I tested several models, including Llama 3.1, which I ran locally. Ultimately, I concluded that OpenAI remains the leader in this space, as it provides the most natural and fluent interactions while avoiding common pitfalls encountered by other LLMs. Here’s a basic example: when greeting the model powered by OpenAI, the response is as follows: ![096c6e75-74b6-44e1-924f-1935027aa7ef](https://static.spring.io/blog/contentful/20240923/096c6e75-74b6-44e1-924f-1935027aa7ef.png) Perfect. Just what I wanted. Simple, concise, professional and user friendly. Here’s the result using Llama3.1: ![215a8acd-337e-4c63-b875-803f13daf146](https://static.spring.io/blog/contentful/20240923/215a8acd-337e-4c63-b875-803f13daf146.png) You get the point. It’s just not there yet. Setting the desired LLM provider is straightforward - simply set its dependency in the `pom.xml` (or `build.gradle`) and provide the necessary configuration properties in `application.yaml` or `application.properties`: ```xml theme={null} org.springframework.ai spring-ai-azure-openai-spring-boot-starter ``` Here, I opted for Azure’s implementation of OpenAI, but I could easily switch to Sam Altman’s OpenAI by changing the dependency: ```xml theme={null} org.springframework.ai spring-ai-openai-spring-boot-starter ``` Since I’m using a publicly hosted LLM provider, I need to provide the URL and API key to access the LLM. This can be configured in `application.yaml`: ```yaml theme={null} spring: ai: #These parameters apply when using the spring-ai-azure-openai-spring-boot-starter dependency: azure: openai: api-key: "the-api-key" endpoint: "https://the-url/" chat: options: deployment-name: "gpt-4o" #These parameters apply when using the spring-ai-openai-spring-boot-starter dependency: openai: api-key: "" endpoint: "" chat: options: deployment-name: "gpt-4o" ``` ## Let's get coding! Our goal is to create a WhatsApp/iMessage-style chat client that integrates with the existing UI of Spring Petclinic. The frontend UI will make calls to a backend API endpoint that accepts a string as input and returns a string as output. The conversation will be open to any questions the user may have, and if we can't assist with a particular request, we'll provide an appropriate response. ### Creating the ChatClient Here’s the implementation for the chat endpoint in the class `PetclinicChatClient`: ```java theme={null} @PostMapping("/chatclient") public String exchange(@RequestBody String query) { //All chatbot messages go through this endpoint and are passed to the LLM return this.chatClient .prompt() .user( u -> u.text(query) ) .call() .content(); } ``` The API accepts a string query and passes it to the Spring AI `ChatClient` bean as user text. The `ChatClient` is a Spring Bean provided by Spring AI that manages sending the user text to the LLM and returning the results in the `content()`. > All the Spring AI code operates under a specific `@Profile` called `openai`. An additional class, `PetclinicDisabledChatClient`, runs when using the default profile or any other profile. This disabled profile simply returns a message indicating that chat is not available. Our implementation primarily delegates responsibility to the `ChatClient`. But how do we create the `ChatClient` bean itself? There are several configurable options that can influence the user experience. Let’s explore them one by one and examine their impact on the final application: #### A Simple ChatClient: Here’s a barebones, unaltered `ChatClient` bean definition: ```java theme={null} public PetclinicChatClient(ChatClient.Builder builder) { this.chatClient = builder.build(); } ``` Here, we simply request an instance of the `ChatClient` from the builder, based on the currently available Spring AI starter in the dependencies. While this setup works, our chat client lacks any knowledge of the Petclinic domain or its services: ![0e879adf-ce3a-460a-a5d2-fcbfcf0af91e](https://static.spring.io/blog/contentful/20240923/0e879adf-ce3a-460a-a5d2-fcbfcf0af91e.png) ![54d23cf7-34bb-400c-9260-fb2d684df98d](https://static.spring.io/blog/contentful/20240923/54d23cf7-34bb-400c-9260-fb2d684df98d.png) It’s certainly polite, but it lacks any understanding of our business domain. Additionally, it seems to suffer from a severe case of amnesia—it can't even remember my name from the previous message! > As I reviewed this article, I realized I’m not [following the advice](https://youtu.be/XUz4LKZx83g?si=EPsh8EJQ0MFnviQa\&t=3828) of my good friend and colleague Josh Long. I should probably be more polite to our new AI overlords! You might be accustomed to ChatGPT's excellent memory, which makes it feel conversational. In reality, however, LLM APIs are entirely stateless and do not retain any of the past messages you send. This is why the API forgot my name so quickly. You may be wondering how ChatGPT maintains conversational context. The answer is simple: ChatGPT sends past messages as content along with each new message. Every time you send a new message, it includes the previous conversations for the model to reference. While this might seem wasteful, it’s just how the system operates. This is also why larger token windows are becoming increasingly important—users expect to revisit conversations from days ago and pick up right where they left off. #### A ChatClient with better memory Let’s implement a similar “chat memory” functionality in our application. Fortunately, Spring AI provides an out-of-the-box Advisor to help with this. You can think of advisors as hooks that run before invoking the LLM. It’s helpful to consider them as resembling Aspect-Oriented Programming advice, even if they aren’t implemented that way. Here’s our updated code: ```java theme={null} public PetclinicChatClient(ChatClient.Builder builder, ChatMemory chatMemory) { // @formatter:off this.chatClient = builder .defaultAdvisors( // Chat memory helps us keep context when using the chatbot for up to 10 previous messages. new MessageChatMemoryAdvisor(chatMemory, DEFAULT_CHAT_MEMORY_CONVERSATION_ID, 10), // CHAT MEMORY new SimpleLoggerAdvisor() ) .build(); } ``` In this updated code, we added the `MessageChatMemoryAdvisor`, which automatically chains the last 10 messages into any new outgoing message, helping the LLM understand the context. We also included an out-of-the-box `SimpleLoggerAdvisor`, which logs the requests and responses to and from the LLM. The result: ![b551498e-bf8e-4d3a-8d77-9ff66d929dfc](https://static.spring.io/blog/contentful/20240923/b551498e-bf8e-4d3a-8d77-9ff66d929dfc.png) Our new chatbot has significantly better memory! However, it’s still not entirely clear on what we’re really doing here: ![ade2c93a-5ef1-4e3f-ab1b-b3589ce332a5](https://static.spring.io/blog/contentful/20240923/ade2c93a-5ef1-4e3f-ab1b-b3589ce332a5.png) This response is decent for a generic world-knowledge LLM. However, our clinic is very domain-specific, with particular use cases. Additionally, our chatbot should focus solely on assisting us with our clinic. For example, it should not attempt to answer a question like this: ![6537a0ba-20a8-47f5-a021-15c4c91f4840](https://static.spring.io/blog/contentful/20240923/6537a0ba-20a8-47f5-a021-15c4c91f4840.png) If we allowed our chatbot to answer any question, users might start using it as a free alternative to services like ChatGPT to access more advanced models like GPT-4. It’s clear that we need to teach our LLM to “impersonate” a specific service provider. Our LLM should focus solely on assisting with Spring Petclinic; it should know about vets, owners, pets, and visits—nothing more. #### **A ChatClient Bound to a Specific Domain** Spring AI offers a solution for this as well. Most LLMs differentiate between user text (the chat messages we send) and system text, which is general text that instructs the LLM to function in a specific manner. Let’s add the system text to our chat client: ```java theme={null} public PetclinicChatClient(ChatClient.Builder builder, ChatMemory chatMemory) { // @formatter:off this.chatClient = builder .defaultSystem(""" You are a friendly AI assistant designed to help with the management of a veterinarian pet clinic called Spring Petclinic. Your job is to answer questions about the existing veterinarians and to perform actions on the user's behalf, mainly around veterinarians, pet owners, their pets and their owner's visits. You are required to answer an a professional manner. If you don't know the answer, politely tell the user you don't know the answer, then ask the user a followup qusetion to try and clarify the question they are asking. If you do know the answer, provide the answer but do not provide any additional helpful followup questions. When dealing with vets, if the user is unsure about the returned results, explain that there may be additional data that was not returned. Only if the user is asking about the total number of all vets, answer that there are a lot and ask for some additional criteria. For owners, pets or visits - answer the correct data. """) .defaultAdvisors( // Chat memory helps us keep context when using the chatbot for up to 10 previous messages. new MessageChatMemoryAdvisor(chatMemory, DEFAULT_CHAT_MEMORY_CONVERSATION_ID, 10), // CHAT MEMORY new LoggingAdvisor() ) .build(); } ``` That's quite a verbose default system prompt! But trust me, it’s necessary. In fact, it’s probably not enough, and as the system is used more frequently, I’ll likely need to add more context. The process of prompt engineering involves designing and optimizing input prompts to elicit specific, accurate responses for a given use case. LLMs are quite chatty; they enjoy responding in natural language. This tendency can make it challenging to get machine-to-machine responses in formats like JSON. To address this, Spring AI offers a feature set dedicated to structured output, known as the [Structured Output Converter](https://docs.spring.io/spring-ai/reference/api/structured-output-converter.html). The Spring team had to identify optimal prompt engineering techniques to ensure that LLMs respond without unnecessary “chattiness.” Here’s an example from Spring AI’s `MapOutputConverter` bean: ````java theme={null} @Override public String getFormat() { String raw = """ Your response should be in JSON format. The data structure for the JSON should match this Java class: %s Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation. Remove the ```json markdown surrounding the output including the trailing "```". """; return String.format(raw, HashMap.class.getName()); } ```` Whenever a response from an LLM needs to be in JSON format, Spring AI appends this entire string to the request, urging the LLM to comply. Recently, there have been positive advancements in this area, particularly with OpenAI’s Structured Outputs initiative. As is often the case with such advancements, [Spring AI has embraced it wholeheartedly](https://spring.io/blog/2024/08/09/spring-ai-embraces-openais-structured-outputs-enhancing-json-response). Now, back to our chatbot—let’s see how it performs! ![22543742-6c1a-426e-aa23-3a16181e3e7e](https://static.spring.io/blog/contentful/20240923/22543742-6c1a-426e-aa23-3a16181e3e7e.png) ![bcea015e-8707-4ddc-adeb-a534e0d28725](https://static.spring.io/blog/contentful/20240923/bcea015e-8707-4ddc-adeb-a534e0d28725.png) ![4f92d3bf-f1d9-44d1-8d7b-c8c7f3a39775](https://static.spring.io/blog/contentful/20240923/4f92d3bf-f1d9-44d1-8d7b-c8c7f3a39775.png) That’s a significant improvement! We now have a chatbot that’s tuned to our domain, focused on our specific use cases, remembers the last 10 messages, doesn’t provide any irrelevant world knowledge, and avoids hallucinating data it doesn’t possess. Additionally, our logs print the calls we’re making to the LLM, making debugging much easier. ```yaml theme={null} 2024-09-21T21:55:08.888+03:00 DEBUG 85824 --- [nio-8080-exec-5] o.s.a.c.c.advisor.SimpleLoggerAdvisor : request: AdvisedRequest[chatModel=org.springframework.ai.azure.openai.AzureOpenAiChatModel@5cdd90c4, userText="Hi! My name is Oded.", systemText=You are a friendly AI assistant designed to help with the management of a veterinarian pet clinic called Spring Petclinic. Your job is to answer questions about the existing veterinarians and to perform actions on the user's behalf, mainly around veterinarians, pet owners, their pets and their owner's visits. You are required to answer an a professional manner. If you don't know the answer, politely tell the user you don't know the answer, then ask the user a followup qusetion to try and clarify the question they are asking. If you do know the answer, provide the answer but do not provide any additional helpful followup questions. When dealing with vets, if the user is unsure about the returned results, explain that there may be additional data that was not returned. Only if the user is asking about the total number of all vets, answer that there are a lot and ask for some additional criteria. For owners, pets or visits - answer the correct data. , chatOptions=org.springframework.ai.azure.openai.AzureOpenAiChatOptions@c4c74d4, media=[], functionNames=[], functionCallbacks=[], messages=[], userParams={}, systemParams={}, advisors=[org.springframework.ai.chat.client.advisor.observation.ObservableRequestResponseAdvisor@1e561f7, org.springframework.ai.chat.client.advisor.observation.ObservableRequestResponseAdvisor@79348b22], advisorParams={}] 2024-09-21T21:55:10.594+03:00 DEBUG 85824 --- [nio-8080-exec-5] o.s.a.c.c.advisor.SimpleLoggerAdvisor : response: {"result":{"metadata":{"contentFilterMetadata":{"sexual":{"severity":"safe","filtered":false},"violence":{"severity":"safe","filtered":false},"hate":{"severity":"safe","filtered":false},"selfHarm":{"severity":"safe","filtered":false},"profanity":null,"customBlocklists":null,"error":null,"protectedMaterialText":null,"protectedMaterialCode":null},"finishReason":"stop"},"output":{"messageType":"ASSISTANT","metadata":{"finishReason":"stop","choiceIndex":0,"id":"chatcmpl-A9zY6UlOdkTCrFVga9hbzT0LRRDO4","messageType":"ASSISTANT"},"toolCalls":[],"content":"Hello, Oded! How can I assist you today at Spring Petclinic?"}},"metadata":{"id":"chatcmpl-A9zY6UlOdkTCrFVga9hbzT0LRRDO4","model":"gpt-4o-2024-05-13","rateLimit":{"requestsLimit":0,"requestsRemaining":0,"requestsReset":0.0,"tokensRemaining":0,"tokensLimit":0,"tokensReset":0.0},"usage":{"promptTokens":633,"generationTokens":17,"totalTokens":650},"promptMetadata":[{"contentFilterMetadata":{"sexual":null,"violence":null,"hate":null,"selfHarm":null,"profanity":null,"customBlocklists":null,"error":null,"jailbreak":null,"indirectAttack":null},"promptIndex":0}],"empty":false},"results":[{"metadata":{"contentFilterMetadata":{"sexual":{"severity":"safe","filtered":false},"violence":{"severity":"safe","filtered":false},"hate":{"severity":"safe","filtered":false},"selfHarm":{"severity":"safe","filtered":false},"profanity":null,"customBlocklists":null,"error":null,"protectedMaterialText":null,"protectedMaterialCode":null},"finishReason":"stop"},"output":{"messageType":"ASSISTANT","metadata":{"finishReason":"stop","choiceIndex":0,"id":"chatcmpl-A9zY6UlOdkTCrFVga9hbzT0LRRDO4","messageType":"ASSISTANT"},"toolCalls":[],"content":"Hello, Oded! How can I assist you today at Spring Petclinic?"}}]} ``` ## Identifying Core Functionality Our chatbot behaves as expected, but it currently lacks knowledge about the data in our application. Let’s focus on the core features that Spring Petclinic supports and map them to the functions we might want to enable with Spring AI: #### List Owners In the Owners tab, we can search for an owner by last name or simply list all owners. We can obtain detailed information about each owner, including their first and last names, as well as the pets they own and their types: ![ed1a033e-6389-47a5-8c9b-d1889bd6e9de](https://static.spring.io/blog/contentful/20240923/ed1a033e-6389-47a5-8c9b-d1889bd6e9de.png) #### **Adding an Owner** The application allows you to add a new owner by providing the required parameters dictated by the system. An owner must have a first name, a last name, an address, and a 10-digit phone number. ![6c6f580d-7cbf-4055-8a2d-e4f43e5f6c1f](https://static.spring.io/blog/contentful/20240923/6c6f580d-7cbf-4055-8a2d-e4f43e5f6c1f.png) #### **Adding a Pet to an Existing Owner** An owner can have multiple pets. The pet types are limited to the following: cat, dog, lizard, snake, bird, or hamster. ![6f6e20ca-28b8-4093-8e4a-31c6f7ffd032](https://static.spring.io/blog/contentful/20240923/6f6e20ca-28b8-4093-8e4a-31c6f7ffd032.png) #### **Veterinarians** The Veterinarians tab displays the available veterinarians in a paginated view, along with their specialties. There is currently no search capability in this tab. While the `main` branch of Spring Petclinic features a handful of vets, I generated hundreds of mock vets in the `spring-ai` branch to simulate an application that handles a substantial amount of data. Later, we will explore how we can use Retrieval-Augmented Generation (RAG) to manage large datasets such as this. ![d055326a-93a4-4c17-a14f-b7a66ed7beea](https://static.spring.io/blog/contentful/20240923/d055326a-93a4-4c17-a14f-b7a66ed7beea.png) These are the main operations we can perform in the system. We’ve mapped our application to its basic functions, and we’d like OpenAI to infer requests in natural language corresponding to these operations. ## Function Calling with Spring AI In the previous section, we described four different functions. Now, let’s map them to functions we can use with Spring AI by specifying specific `java.util.function.Function` beans. #### List Owners The following `java.util.function.Function` is responsible for returning the list of owners in Spring Petclinic: ```java theme={null} @Configuration @Profile("openai") class AIFunctionConfiguration { // The @Description annotation helps the model understand when to call the function @Bean @Description("List the owners that the pet clinic has") public Function listOwners(AIDataProvider petclinicAiProvider) { return request -> { return petclinicAiProvider.getAllOwners(); }; } } record OwnerRequest(Owner owner) { }; record OwnersResponse(List owners) { }; ``` * We’re creating a `@Configuration` class in the `openai` profile, where we register a standard Spring `@Bean`. * The bean must return a `java.util.function.Function`. * We use Spring's `@Description` annotation to explain what this function does. Notably, Spring AI will pass this description to the LLM to help it determine when to call this specific function. * The function accepts an `OwnerRequest` record, which holds the existing Spring Petclinic Owner entity class. This demonstrates how Spring AI can leverage components you've already developed in your application without requiring a complete rewrite. * OpenAI will decide when to invoke the function with a JSON object representing the `OwnerRequest` record. Spring AI will automatically convert this JSON into an `OwnerRequest` object and execute the function. Once a response is returned, Spring AI will convert the resulting `OwnerResponse` record—which holds a `List`—back to JSON format for OpenAI to process. When OpenAI receives the response, it will craft a reply for the user in natural language. * The function calls an `AIDataProvider` `@Service` bean that implements the actual logic. In our simple use case, the function merely queries the data using JPA: ```java theme={null} public OwnersResponse getAllOwners() { Pageable pageable = PageRequest.of(0, 100); Page ownerPage = ownerRepository.findAll(pageable); return new OwnersResponse(ownerPage.getContent()); } ``` * The existing legacy code of Spring Petclinic returns paginated data to keep the response size manageable and facilitate processing for the paginated view in the UI. In our case, we expect the total number of owners to be relatively small, and OpenAI should be able to handle such traffic in a single request. Therefore, we return the first 100 owners in a single JPA request. You may be thinking that this approach isn't optimal, and in a real-world application, you would be correct. If there were a large amount of data, this method would be inefficient—it's likely we’d have more than 100 owners in the system. For such scenarios, we would need to implement a different pattern, as we will explore in the `listVets` function. However, for our demo use case, we can assume our system contains fewer than 100 owners. Let’s use a real example along with the `SimpleLoggerAdvisor` to observe what happens behind the scenes: ![f5ee0306-5463-4dc6-96de-908b7af37d9e](https://static.spring.io/blog/contentful/20240923/f5ee0306-5463-4dc6-96de-908b7af37d9e.png) What happened here? Let’s review the output from the `SimpleLoggerAdvisor` log to investigate: ```yaml theme={null} request: AdvisedRequest[chatModel=org.springframework.ai.azure.openai.AzureOpenAiChatModel@18e69455, userText= "List the owners that are called Betty.", systemText=You are a friendly AI assistant designed to help with the management of a veterinarian pet clinic called Spring Petclinic. Your job... chatOptions=org.springframework.ai.azure.openai.AzureOpenAiChatOptions@3d6f2674, media=[], functionNames=[], functionCallbacks=[], messages=[UserMessage{content='"Hi there!"', properties={messageType=USER}, messageType=USER}, AssistantMessage [messageType=ASSISTANT, toolCalls=[], textContent=Hello! How can I assist you today at Spring Petclinic?, metadata={choiceIndex=0, finishReason=stop, id=chatcmpl-A99D20Ql0HbrpxYc0LIkWZZLVIAKv, messageType=ASSISTANT}]], userParams={}, systemParams={}, advisors=[org.springframework.ai.chat.client.advisor.observation.ObservableRequestResponseAdvisor@1d04fb8f, org.springframework.ai.chat.client.advisor.observation.ObservableRequestResponseAdvisor@2fab47ce], advisorParams={}] ``` The request contains interesting data about what is sent to the LLM, including the user text, historical messages, an ID representing the current chat session, the list of advisors to trigger, and the system text. You might be wondering where the functions are in the logged request above. The functions are not explicitly logged; they are encapsulated within the contents of `AzureOpenAiChatOptions`. Examining the object in debug mode reveals the list of functions available to the model: ![f2ebab0c-aef1-49ba-ad64-c5cd79d8583e](https://static.spring.io/blog/contentful/20240923/f2ebab0c-aef1-49ba-ad64-c5cd79d8583e.png) OpenAI will process the request, determine that it requires data from the list of owners, and return a JSON reply to Spring AI requesting additional information from the `listOwners` function. Spring AI will then invoke that function using the provided `OwnersRequest` object from OpenAI and send the response back to OpenAI, maintaining the conversation ID to assist with session continuity over the stateless connection. OpenAI will generate the final response based on the additional data provided. Let’s review that response as it is logged: ```json theme={null} response: { "result": { "metadata": { "finishReason": "stop", "contentFilterMetadata": { "sexual": { "severity": "safe", "filtered": false }, "violence": { "severity": "safe", "filtered": false }, "hate": { "severity": "safe", "filtered": false }, "selfHarm": { "severity": "safe", "filtered": false }, "profanity": null, "customBlocklists": null, "error": null, "protectedMaterialText": null, "protectedMaterialCode": null } }, "output": { "messageType": "ASSISTANT", "metadata": { "choiceIndex": 0, "finishReason": "stop", "id": "chatcmpl-A9oKTs6162OTut1rkSKPH1hE2R08Y", "messageType": "ASSISTANT" }, "toolCalls": [], "content": "The owner named Betty in our records is:\n\n- **Betty Davis**\n - **Address:** 638 Cardinal Ave., Sun Prairie\n - **Telephone:** 608-555-1749\n - **Pet:** Basil (Hamster), born on 2012-08-06\n\nIf you need any more details or further assistance, please let me know!" } }, ... ] } ``` We see the response itself in the `content` section. Most of the returned JSON consists of metadata—such as content filters, the model being used, the chat ID session in the response, the number of tokens consumed, how the response completed, and more. This illustrates how the system operates end-to-end: it starts in your browser, reaches the Spring backend, and involves a B2B ping-pong interaction between Spring AI and the LLM until a response is sent back to the JavaScript that made the initial call. Now, let’s review the remaining three functions. #### Add Pet to Owner The `addPetToOwner` method is particularly interesting because it demonstrates the power of the model’s function calling. When a user wants to add a pet to an owner, it’s unrealistic to expect them to input the pet type ID. Instead, they are likely to say the pet is a "dog" rather than simply providing a numeric ID like "2”. To assist the LLM in determining the correct pet type, I utilized the @Description annotation to provide hints about our requirements. Since our pet clinic only deals with six types of pets, this approach is manageable and effective: ```java theme={null} @Bean @Description("Add a pet with the specified petTypeId, " + "to an owner identified by the ownerId. " + "The allowed Pet types IDs are only: " + "1 - cat" + "2 - dog" + "3 - lizard" + "4 - snake" + "5 - bird" + "6 - hamster") public Function addPetToOwner(AIDataProvider petclinicAiProvider) { return request -> { return petclinicAiProvider.addPetToOwner(request); }; } ``` The `AddPetRequest` record includes the pet type in free text, reflecting how a user would typically provide it, along with the complete Pet entity and the referenced `ownerId`. ```java theme={null} record AddPetRequest(Pet pet, String petType, Integer ownerId) { }; record AddedPetResponse(Owner owner) { }; ``` Here’s the business implementation: we retrieve the owner by their ID and then add the new pet to their existing list of pets. ```java theme={null} public AddedPetResponse addPetToOwner(AddPetRequest request) { Owner owner = ownerRepository.findById(request.ownerId()); owner.addPet(request.pet()); this.ownerRepository.save(owner); return new AddedPetResponse(owner); } ``` While debugging the flow for this article, I noticed an interesting behavior: in some cases, the `Pet` entity in the request was already prepopulated with the correct pet type ID and name. ![787f442b-2b02-4499-9516-223d09c77128](https://static.spring.io/blog/contentful/20240923/787f442b-2b02-4499-9516-223d09c77128.png) I also noticed that I wasn't really using the `petType` String in my business implementation. Is it possible that Spring AI simply "figured out" the correct mapping of the `PetType` name to the correct ID on its own? To test this, I removed the `petType` from my request object and simplified the `@Description` as well: ```java theme={null} @Bean @Description("Add a pet with the specified petTypeId, to an owner identified by the ownerId.") public Function addPetToOwner(AIDataProvider petclinicAiProvider) { return request -> { return petclinicAiProvider.addPetToOwner(request); }; } record AddPetRequest(Pet pet, Integer ownerId) { }; record AddedPetResponse(Owner owner) { }; ``` I found that in most prompts, the LLM remarkably figured out how to perform the mapping on its own. I eventually did keep the original description in the PR, because I noticed some edge cases where the LLM struggled and failed to understand the correlation. Still, even for 80% of the use cases, this was very impressive. These are the sort of things that make Spring AI and LLMs almost feel like magic. The interaction between Spring AI and OpenAI managed to understand that the `PetType` in the `@Entity` of `Pet` needed the mapping of the String "lizard" to its corresponding ID value in the database. This kind of seamless integration showcases the potential of combining traditional programming with AI capabilities. ```java theme={null} // These are the original insert queries in data.sql INSERT INTO types VALUES (default, 'cat'); //1 INSERT INTO types VALUES (default, 'dog'); //2 INSERT INTO types VALUES (default, 'lizard'); //3 INSERT INTO types VALUES (default, 'snake'); //4 INSERT INTO types VALUES (default, 'bird'); //5 INSERT INTO types VALUES (default, 'hamster'); //6 @Entity @Table(name = "pets") public class Pet extends NamedEntity { private static final long serialVersionUID = 622048308893169889L; @Column(name = "birth_date") @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate birthDate; @ManyToOne @JoinColumn(name = "type_id") private PetType type; @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) @JoinColumn(name = "pet_id") @OrderBy("visit_date ASC") private Set visits = new LinkedHashSet<>(); ``` It even works if you make some typos in the request. In the example below, the LLM identified that I misspelled "hamster" as "hamstr," corrected the request, and successfully matched it with the correct Pet ID: ![612140cf-0bfd-441e-b481-984a1814a840](https://static.spring.io/blog/contentful/20240923/612140cf-0bfd-441e-b481-984a1814a840.png) If you dig deeper, you'll find things become even more impressive. The `AddPetRequest` only passes the `ownerId` as a parameter; I provided the owner's first name instead of their ID, and the LLM managed to determine the correct mapping on its own. This indicates that the LLM chose to call the `listOwners` function before invoking the `addPetToOwner` function. By adding some breakpoints, we can confirm this behavior. Initially, we hit the breakpoint for retrieving the owners: ![73e60339-4472-4fb9-bb7d-d23fb4680faf](https://static.spring.io/blog/contentful/20240923/73e60339-4472-4fb9-bb7d-d23fb4680faf.png) Only after the owner data is returned and processed do we invoke the `addPetToOwner` function: ![4c3ae801-b1f8-4477-924f-3e4d10c137df](https://static.spring.io/blog/contentful/20240923/4c3ae801-b1f8-4477-924f-3e4d10c137df.png) My conclusion is this: with Spring AI, start simple. Provide the essential data you know is required and use short, concise bean descriptions. It’s likely that Spring AI and the LLM will "figure out" the rest. Only when issues arise should you begin adding more hints to the system. #### Add Owner The `addOwner` function is relatively straightforward. It accepts an owner and adds him/her to the system. However, in this example, we can see how to perform validation and ask follow-up questions using our chat assistant: ```java theme={null} @Bean @Description("Add a new pet owner to the pet clinic. " + "The Owner must include first and last name, " + "an address and a 10-digit phone number") public Function addOwnerToPetclinic(AIDataProvider petclinicAiDataProvider) { return request -> { return petclinicAiDataProvider.addOwnerToPetclinic(request); }; } record OwnerRequest(Owner owner) { }; record OwnerResponse(Owner owner) { }; ``` The business implementation is straightforward: ```java theme={null} public OwnerResponse addOwnerToPetclinic(OwnerRequest ownerRequest) { ownerRepository.save(ownerRequest.owner()); return new OwnerResponse(ownerRequest.owner()); } ``` Here, we guide the model to ensure that the `Owner` within the `OwnerRequest` meets certain validation criteria before it can be added. Specifically, the owner must include a first name, a last name, an address, and a 10-digit phone number. If any of this information is missing, the model will prompt us to provide the necessary details before proceeding with the addition of the owner: ![7d3d4244-dd6d-47b0-a620-833e7ad1601d](https://static.spring.io/blog/contentful/20240923/7d3d4244-dd6d-47b0-a620-833e7ad1601d.png) The model didn't create the new owner before requesting the necessary additional data, such as the address, city, and phone number. However, I don't recall providing the required last name. Will it still work? ![429dca17-1dbd-4444-8282-b629a6320423](https://static.spring.io/blog/contentful/20240923/429dca17-1dbd-4444-8282-b629a6320423.png) We've identified an edge case in the model: it doesn't seem to enforce the requirement for a last name, even though the `@Description` specifies that it is mandatory. How can we address this? Prompt engineering to the rescue! ```java theme={null} @Bean @Description("Add a new pet owner to the pet clinic. " + "The Owner must include a first name and a last name as two separate words, " + "plus an address and a 10-digit phone number") public Function addOwnerToPetclinic(AIDataProvider petclinicAiDataProvider) { return request -> { return petclinicAiDataProvider.addOwnerToPetclinic(request); }; } ``` By adding the hint "as two separate words" to our description, the model gained clarity on our expectations, allowing it to correctly enforce the requirement for the last name. ![a3f4d7c3-5b7c-46eb-9b0a-f828f338b40c](https://static.spring.io/blog/contentful/20240923/a3f4d7c3-5b7c-46eb-9b0a-f828f338b40c.png) ![26e59430-d5ab-4cf0-86a7-f49fc51016ee](https://static.spring.io/blog/contentful/20240923/26e59430-d5ab-4cf0-86a7-f49fc51016ee.png) ![ff606400-d784-47cb-a34a-0319849ec0fe](https://static.spring.io/blog/contentful/20240923/ff606400-d784-47cb-a34a-0319849ec0fe.png) ### Next Steps In the first part of this article, we explored how to harness Spring AI to work with large language models. We built a custom ChatClient, utilized Function Calling, and refined prompt engineering for our specific needs. In [Part II](https://spring.io/blog/2024/09/27/ai-meets-spring-petclinic-implementing-an-ai-assistant-with-spring-ai-part), we’ll dive into the power of Retrieval-Augmented Generation (RAG) to integrate the model with large, domain-specific datasets that are too extensive to fit within the Function Calling approach. # AI Meets Spring Petclinic: Implementing an AI Assistant with Spring AI (Part II) Source: https://springaicommunity.mintlify.app/blog/advisors/petclinic-part-2 In the [first part](https://spring.io/blog/2024/09/26/ai-meets-spring-petclinic-implementing-an-ai-assistant-with-spring-ai-part-i) of this blog series, we... ## Recap of Part I In the [first part](https://spring.io/blog/2024/09/26/ai-meets-spring-petclinic-implementing-an-ai-assistant-with-spring-ai-part-i) of this blog series, we explored the basics of integrating Spring AI with large language models. We walked through building a custom ChatClient, leveraging Function Calling for dynamic interactions, and refining our prompts to suit the Spring Petclinic use case. By the end, we had a functional AI assistant capable of understanding and processing requests related to our veterinary clinic domain. Now, in Part II, we’ll go a step further by exploring Retrieval-Augmented Generation (RAG), a technique that enables us to handle large datasets that wouldn’t fit within the constraints of a typical Function Calling approach. Let’s see how RAG can seamlessly integrate AI with domain-specific knowledge. ## Retrieval-Augmented Generation While listing veterinarians could have been a straightforward implementation, I chose this as an opportunity to showcase the power of Retrieval-Augmented Generation (RAG). RAG integrates large language models with real-time data retrieval to produce more accurate and contextually relevant text. Although this concept aligns with our previous work, RAG typically emphasizes data retrieval from a vector store. A vector store contains data in the form of *embeddings*—numerical representations that capture the meaning of the information, such as the data about our veterinarians. These embeddings are stored as high-dimensional vectors, facilitating efficient *similarity searches* based on *semantics* rather than traditional text-based searches. For instance, consider the following veterinarians and their specialties: 1. Dr. Alice Brown - **Cardiology** 2. Dr. Bob Smith - **Dentistry** 3. Dr. Carol White - **Dermatology** In a conventional search, a query for "Teeth Cleaning" would yield no exact matches. However, with semantic search powered by embeddings, the system recognizes that "Teeth Cleaning" relates to "Dentistry." Consequently, Dr. Bob Smith would be returned as the best match, even though his specialty was never explicitly mentioned in the query. This illustrates how embeddings capture the underlying *meaning* rather than merely relying on exact keywords. While the implementation of this process is beyond the scope of this article, you can learn more by checking out this [YouTube video](https://youtu.be/eMlx5fFNoYc?si=Lyjb1doyeAt3_EKG). > Fun fact - this example was [generated by ChatGPT itself.](https://chatgpt.com/share/66eef934-696c-8008-9961-93da0740c8f2) In essence, similarity searches operate by identifying the nearest numerical values of the search query to those of the source data. The closest match is returned. The process of transforming text into these numerical embeddings is also handled by the LLM. ### Generating Test Data Utilizing a vector store is most effective when handling a substantial amount of data. Given that six veterinarians can easily be processed in a single call to the LLM, I aimed to increase the number to 256. While even 256 may still be relatively small, it serves well for illustrating our process. Veterinarians in this setup can have zero, one, or two specialties, mirroring the original examples from Spring Petclinic. To avoid the tedious task of creating all this mock data manually, I enlisted ChatGPT's assistance. It generated a union query that produces 250 veterinarians and assigns specialties to 80% of them: ```sql theme={null} -- Create a list of first names and last names WITH first_names AS ( SELECT 'James' AS name UNION ALL SELECT 'Mary' UNION ALL SELECT 'John' UNION ALL ... ), last_names AS ( SELECT 'Smith' AS name UNION ALL SELECT 'Johnson' UNION ALL SELECT 'Williams' UNION ALL ... ), random_names AS ( SELECT first_names.name AS first_name, last_names.name AS last_name FROM first_names CROSS JOIN last_names ORDER BY RAND() LIMIT 250 ) INSERT INTO vets (first_name, last_name) SELECT first_name, last_name FROM random_names; -- Add specialties for 80% of the vets WITH vet_ids AS ( SELECT id FROM vets ORDER BY RAND() LIMIT 200 -- 80% of 250 ), specialties AS ( SELECT id FROM specialties ), random_specialties AS ( SELECT vet_ids.id AS vet_id, specialties.id AS specialty_id FROM vet_ids CROSS JOIN specialties ORDER BY RAND() LIMIT 300 -- 2 specialties per vet on average ) INSERT INTO vet_specialties (vet_id, specialty_id) SELECT vet_id, specialty_id FROM ( SELECT vet_id, specialty_id, ROW_NUMBER() OVER (PARTITION BY vet_id ORDER BY RAND()) AS rn FROM random_specialties ) tmp WHERE rn <= 2; -- Assign at most 2 specialties per vet -- The remaining 20% of vets will have no specialties, so no need for additional insertion commands ``` To ensure that my data remains static and consistent across runs, I exported the relevant tables from the H2 database as hardcoded insert statements. These statements were then added to the `data.sql` file: ```sql theme={null} INSERT INTO vets VALUES (default, 'James', 'Carter'); INSERT INTO vets VALUES (default, 'Helen', 'Leary'); INSERT INTO vets VALUES (default, 'Linda', 'Douglas'); INSERT INTO vets VALUES (default, 'Rafael', 'Ortega'); INSERT INTO vets VALUES (default, 'Henry', 'Stevens'); INSERT INTO vets VALUES (default, 'Sharon', 'Jenkins'); INSERT INTO vets VALUES (default, 'Matthew', 'Alexander'); INSERT INTO vets VALUES (default, 'Alice', 'Anderson'); INSERT INTO vets VALUES (default, 'James', 'Rogers'); INSERT INTO vets VALUES (default, 'Lauren', 'Butler'); INSERT INTO vets VALUES (default, 'Cheryl', 'Rodriguez'); ... ... -- Total of 256 vets -- First, let's make sure we have 5 specialties INSERT INTO specialties (name) VALUES ('radiology'); INSERT INTO specialties (name) VALUES ('surgery'); INSERT INTO specialties (name) VALUES ('dentistry'); INSERT INTO specialties (name) VALUES ('cardiology'); INSERT INTO specialties (name) VALUES ('anesthesia'); INSERT INTO vet_specialties VALUES ('220', '2'); INSERT INTO vet_specialties VALUES ('131', '1'); INSERT INTO vet_specialties VALUES ('58', '3'); INSERT INTO vet_specialties VALUES ('43', '4'); INSERT INTO vet_specialties VALUES ('110', '3'); INSERT INTO vet_specialties VALUES ('63', '5'); INSERT INTO vet_specialties VALUES ('206', '4'); INSERT INTO vet_specialties VALUES ('29', '3'); INSERT INTO vet_specialties VALUES ('189', '3'); ... ... ``` ### Embedding the Test Data We have several options available for the vector store itself. Postgres with the pgVector extension is probably the most popular choice. Greenplum—a massively parallel Postgres database—also supports pgVector. The Spring AI [reference documentation](https://docs.spring.io/spring-ai/reference/api/vectordbs.html) lists the currently supported vector stores. For our simple use case, I opted to use the Spring AI-provided `SimpleVectorStore`. This class implements a vector store using a straightforward Java `ConcurrentHashMap`, which is more than sufficient for our small dataset of 256 vets. The configuration for this vector store, along with the chat memory implementation, is defined in the `AIBeanConfiguration` class annotated with `@Configuration`: ```java theme={null} @Configuration @Profile("openai") public class AIBeanConfiguration { @Bean public ChatMemory chatMemory() { return new InMemoryChatMemory(); } @Bean VectorStore vectorStore(EmbeddingModel embeddingModel) { return new SimpleVectorStore(embeddingModel); } } ``` The vector store needs to embed the veterinarian data as soon as the application starts. To achieve this, I added a `VectorStoreController` bean, which includes an `@EventListener` that listens for the `ApplicationStartedEvent`. This method is automatically invoked by Spring as soon as the application is up and running, ensuring that the veterinarian data is embedded into the vector store at the appropriate time: ```java theme={null} @EventListener public void loadVetDataToVectorStoreOnStartup(ApplicationStartedEvent event) throws IOException { // Fetches all Vet entites and creates a document per vet Pageable pageable = PageRequest.of(0, Integer.MAX_VALUE); Page vetsPage = vetRepository.findAll(pageable); Resource vetsAsJson = convertListToJsonResource(vetsPage.getContent()); DocumentReader reader = new JsonReader(vetsAsJson); List documents = reader.get(); // add the documents to the vector store this.vectorStore.add(documents); if (vectorStore instanceof SimpleVectorStore) { var file = File.createTempFile("vectorstore", ".json"); ((SimpleVectorStore) this.vectorStore).save(file); logger.info("vector store contents written to {}", file.getAbsolutePath()); } logger.info("vector store loaded with {} documents", documents.size()); } public Resource convertListToJsonResource(List vets) { ObjectMapper objectMapper = new ObjectMapper(); try { // Convert List to JSON string String json = objectMapper.writeValueAsString(vets); // Convert JSON string to byte array byte[] jsonBytes = json.getBytes(); // Create a ByteArrayResource from the byte array return new ByteArrayResource(jsonBytes); } catch (JsonProcessingException e) { e.printStackTrace(); return null; } } ``` There’s a lot to unpack here, so let’s walk through the code: 1. Similar to `listOwners`, we begin by retrieving all vets from the database. 2. Spring AI embeds entities of type `Document` into the vector store. A `Document` represents the embedded numerical data alongside its original, human-readable text data. This dual representation allows our code to map correlations between the embedded vectors and the natural text. 3. To create these `Document` entities, we need to convert our `Vet` entities into a textual format. Spring AI provides two built-in readers for this purpose: `JsonReader` and `TextReader`. Since our `Vet` entities are structured data, it makes sense to represent them as JSON. To achieve this, we use the helper method `convertListToJsonResource`, which leverages the Jackson parser to convert the list of vets into an in-memory JSON resource. 4. Next, we call the `add(documents)` method on the vector store. This method is responsible for embedding the data by iterating over the list of documents (our vets in JSON format) and embedding each one while associating the original metadata with it. 5. Though not strictly required, we also generate a `vectorstore.json` file, which represents the state of our `SimpleVectorStore` database. This file allows us to observe how Spring AI interprets the stored data behind the scenes. Let’s take a look at the generated file to understand what Spring AI sees. ```json theme={null} { "dd919c71-06bb-4777-b974-120dfee8b9f9" : { "embedding" : [ 0.013877872, 0.03598228, 0.008212427, 0.00917901, -0.036433823, 0.03253927, -0.018089917, -0.0030867155, -0.0017038669, -0.048145704, 0.008974405, 0.017624263, 0.017539598, -4.7888185E-4, 0.013842596, -0.0028221398, 0.033414137, -0.02847539, -0.0066955267, -0.021885695, -0.0072387885, 0.01673529, -0.007386951, 0.014661016, -0.015380662, 0.016184973, 0.00787377, -0.019881975, -0.0028785826, -0.023875304, 0.024778388, -0.02357898, -0.023748307, -0.043094076, -0.029322032, ... ], "content" : "{id=31, firstName=Samantha, lastName=Walker, new=false, specialties=[{id=2, name=surgery, new=false}]}", "id" : "dd919c71-06bb-4777-b974-120dfee8b9f9", "metadata" : { }, "media" : [ ] }, "4f9aabed-c15c-43f6-9dbc-46ed9a18e176" : { "embedding" : [ 0.01051745, 0.032714732, 0.007800559, -0.0020621764, -0.03240663, 0.025530376, 0.0037602335, -0.0023702774, -0.004978633, -0.037364256, 0.0012831709, 0.032742742, 0.005430281, 0.00847278, -0.004285406, 0.01146276, 0.03036196, -0.029941821, 0.013220336, -0.03207052, -7.518716E-4, 0.016665466, -0.0052062077, 0.010678503, 0.0026591222, 0.0091940155, ... ], "content" : "{id=195, firstName=Shirley, lastName=Martinez, new=false, specialties=[{id=1, name=radiology, new=false}, {id=2, name=surgery, new=false}]}", "id" : "4f9aabed-c15c-43f6-9dbc-46ed9a18e176", "metadata" : { }, "media" : [ ] }, "55b13970-cd55-476b-b7c9-62337855ae0a" : { "embedding" : [ -0.0031563698, 0.03546827, 0.018778138, -0.01324492, -0.020253662, 0.027756566, 0.007182742, -0.008637386, -0.0075725033, -0.025543278, 5.850768E-4, 0.02568248, 0.0140383635, -0.017330453, 0.003935892, ... ], "content" : "{id=19, firstName=Jacqueline, lastName=Ross, new=false, specialties=[{id=4, name=cardiology, new=false}]}", "id" : "55b13970-cd55-476b-b7c9-62337855ae0a", "metadata" : { }, "media" : [ ] }, ... ... ... ``` Pretty cool! We have a `Vet` in JSON format alongside a set of numbers that, while they might not make much sense to us, are highly meaningful to the LLM. These numbers represent the embedded vector data, which the model uses to understand the relationships and semantics of the `Vet` entity in a way far beyond simple text matching. ### Optimizing for Cost and Fast Startup If we were to run this embedding method on every application restart, it would lead to two significant drawbacks: 1. **Long Startup Times**: Each `Vet` JSON document would need to be re-embedded by making calls to the LLM again, delaying application readiness. 2. **Increased Costs**: Embedding 256 documents would send 256 requests to the LLM every time the app starts, leading to unnecessary usage of LLM credits. Embeddings are better suited for ETL (Extract, Transform, Load) or streaming processes, which run independently of the main web application. These processes can handle embedding in the background without impacting user experience or causing unnecessary cost. To keep things simple in the Spring Petclinic, I decided to load the pre-embedded vector store on startup. This approach provides instant loading and avoids any additional LLM costs. Here’s the addition to the method to achieve that: ```java theme={null} @EventListener public void loadVetDataToVectorStoreOnStartup(ApplicationStartedEvent event) throws IOException { Resource resource = new ClassPathResource("vectorstore.json"); // Check if file exists if (resource.exists()) { // In order to save on AI credits, use a pre-embedded database that was saved // to disk based on the current data in the h2 data.sql file File file = resource.getFile(); ((SimpleVectorStore) this.vectorStore).load(file); logger.info("vector store loaded from existing vectorstore.json file in the classpath"); return; } // Rest of the method as before ... ... } ``` The `vectorstore.json` file is located under `src/main/resources`, ensuring that the application will always load the pre-embedded vector store on startup, rather than re-embedding the data from scratch. If we ever need to regenerate the vector store, we can simply delete the existing `vectorstore.json` file and restart the application. Once the updated vector store is generated, we can place the new `vectorstore.json` file back into `src/main/resources`. This approach gives us flexibility while avoiding unnecessary re-embedding processes during regular restarts. ### **Implementing Similarity Search** With our vector store ready, implementing the `listVets` function becomes straightforward. The function is defined as follows: ```java theme={null} @Bean @Description("List the veterinarians that the pet clinic has") public Function listVets(AIDataProvider petclinicAiProvider) { return request -> { try { return petclinicAiProvider.getVets(request); } catch (JsonProcessingException e) { e.printStackTrace(); return null; } }; } record VetResponse(List vet) { }; record VetRequest(Vet vet) { } ``` Here is the implementation in `AIDataProvider`: ```java theme={null} public VetResponse getVets(VetRequest request) throws JsonProcessingException { ObjectMapper objectMapper = new ObjectMapper(); String vetAsJson = objectMapper.writeValueAsString(request.vet()); SearchRequest sr = SearchRequest.from(SearchRequest.defaults()).withQuery(vetAsJson).withTopK(20); if (request.vet() == null) { // Provide a limit of 50 results when zero parameters are sent sr = sr.withTopK(50); } List topMatches = this.vectorStore.similaritySearch(sr); List results = topMatches.stream().map(document -> document.getContent()).toList(); return new VetResponse(results); } ``` Let's review what we've done here: 1. We start with a `Vet` entity in the request. Since the records in our vector store are represented as JSON, the first step is to convert the `Vet` entity into JSON as well. 2. Next, we create a `SearchRequest`, which is the parameter passed to the `similaritySearch` method of the vector store. The `SearchRequest` allows us to fine-tune the search based on our specific needs. In this case, we mostly use the defaults, except for the `topK` parameter, which determines how many results to return. By default, this is set to 4, but in our case, we increase it to 20. This lets us handle broader queries like “How many vets specialize in cardiology?” 3. If no filters are provided in the request (i.e., the `Vet` entity is empty), we increase the `topK` value to 50. This enables us to return up to 50 vets for queries like “list the vets in the clinic.” Of course, this won't be the entire list, as we want to avoid overwhelming the LLM with too much data. However, we should be fine because we carefully fine-tuned the system text to manage these cases: ```yaml theme={null} When dealing with vets, if the user is unsure about the returned results, explain that there may be additional data that was not returned. Only if the user is asking about the total number of all vets, answer that there are a lot and ask for some additional criteria. For owners, pets or visits - answer the correct data. ``` 4. The final step is to call the `similaritySearch` method. We then map the `getContent()` of each returned result, as this contains the actual Vet JSONs rather than the embedded data. From here, it's business as usual. The LLM completes the function call, retrieves the results, and determines how best to display the data in the chat. Let’s see it in action: ![5e16fa8b-4073-4d4f-ab1a-ca1324a83616](https://static.spring.io/blog/contentful/20240923/5e16fa8b-4073-4d4f-ab1a-ca1324a83616.png) It looks like our system text is functioning as expected, preventing any overload. Now, let’s try providing some specific criteria: ![76dcf069-0393-4749-b730-1f59a5cba3bd](https://static.spring.io/blog/contentful/20240923/76dcf069-0393-4749-b730-1f59a5cba3bd.png) The data returned from the LLM is exactly what we expect. Let’s try a broader question: ![8ec546df-dcb4-4e1d-9f34-891dd39ff9e5](https://static.spring.io/blog/contentful/20240923/8ec546df-dcb4-4e1d-9f34-891dd39ff9e5.png) The LLM successfully identified at least 20 vets specializing in cardiology, adhering to our defined upper limit of topK (20). However, if there’s any uncertainty about the results, the LLM notes that there may be additional vets available, as specified in our system text. ## Implementing the UI Implementing the chatbot UI involves working with Thymeleaf, JavaScript, CSS, and the SCSS preprocessor. After reviewing the code, I decided to place the chatbot in a location accessible from any tab, making `layout.html` the ideal choice. During discussions about the PR with Dr. Dave Syer, I realized that I shouldn't modify `petclinic.css` directly, as Spring Petclinic utilizes an SCSS preprocessor to generate the CSS file. I'll admit—I’m primarily a backend Spring developer with a career focused on Spring, cloud architecture, Kubernetes, and Cloud Foundry. While I have some experience with Angular, I’m not an expert in frontend development. I could probably come up with something, but it likely wouldn’t look polished. Fortunately, I had a great partner for pair programming—ChatGPT. If you're interested in how I developed the UI code, you can check out this [ChatGPT session](https://chatgpt.com/share/8c7accc2-8939-4fd6-8090-6abbb9d4e4cc). It’s remarkable how much you can learn from collaborating with large language models on coding exercises. Just remember to thoroughly review the suggestions instead of blindly copy-pasting them. ## **Conclusion** After experimenting with Spring AI for a few months, I’ve come to deeply appreciate the thought and effort behind this project. Spring AI is truly unique because it allows developers to explore the world of AI without needing to train hundreds of team members in a new language like Python. More importantly, this experience highlights an even greater advantage: your AI code can coexist in the same codebase as your existing business logic. You can easily enhance a legacy codebase with AI capabilities using just a few additional classes. The ability to avoid rebuilding all your data from scratch in a new AI-specific application significantly boosts productivity. Even simple features like automatic code completion for your existing JPA entities in the IDE make a tremendous difference. Spring AI has the potential to significantly enhance Spring-based applications by simplifying the integration of AI capabilities. It empowers developers to leverage machine learning models and AI-powered services without needing deep expertise in data science. By abstracting complex AI operations and embedding them directly into familiar Spring frameworks, developers can focus on rapidly building intelligent, data-driven features. This seamless fusion of AI and Spring fosters an environment where innovation is not hindered by technical barriers, creating new opportunities for developing smarter, more adaptive applications. # Supercharging Your AI Applications with Spring AI Advisors Source: https://springaicommunity.mintlify.app/blog/advisors/spring-ai-advisors In the rapidly evolving world of artificial intelligence, developers are constantly seeking ways to enhance their AI applications. In the rapidly evolving world of artificial intelligence, developers are constantly seeking ways to enhance their AI applications. [Spring AI](https://docs.spring.io/spring-ai/reference/index.html), a Java framework for building AI-powered applications, has introduced a powerful feature: the [Spring AI Advisors](https://docs.spring.io/spring-ai/reference/api/advisors.html). The advisors can supercharge your AI applications, making them more modular, portable and easier to maintain. > If reading the post isn't convenient, you can listen to this **experimental** podcast, **AI-generated** from blog's content: