Building a simple AI assistant with Spring Boot and LangChain4j
Introduction
Since the release of OpenAI’s ChatGPT at the end of 2022, artificial intelligence has become a very hot topic in the IT industry. Many companies are exploring and leveraging the capabilities of AI to enhance the efficiency of their products and to improve overall user experience. As the capabilities of AI continue to advance, there is growing excitement about the potential for further innovations across the market.
AI assistants and chatbots are examples of AI-powered tools that help enhance users’ experience by helping them find their way in an application. They are usually configured to provide all the necessary information by speaking to users in a friendly and polite tone. Such assistants can offer personalized recommendations, answer users’ questions, and even perform some tasks that could save users’ time and effort.
This blog explains the process of building one such AI assistant with Spring Boot and LangChain4j. This assistant will utilize OpenAI’s ChatGPT to serve users by providing them with information about charging stations for electronic vehicles.
LangChain4j
Langchain is a framework for developing applications powered by large language models (LLM). It offers various methods and tools for creating context-aware applications that “understand” what their purpose is and what their functionalities are. This is achieved by connecting the language model to the source of context, such as prompt instructions or a brief explanation of the app. The app then communicates with the language model which provides it with the necessary information about the context based on the current state of the app and potential users’ input. AI assistants need to be context-aware because we don’t want them to make up stories based on their LLM data, but provide reliable and credible information based on the application’s state and data.
LangChain4j is a version of Langchain tailored for JVM apps and frameworks like Spring Boot and Quarkus. This is a relatively new version, whose development began in early 2023, and by the time of writing this article (February 2024) there is still no complete official documentation of it. Nevertheless, the official GitHub repository of LangChain4j contains various examples that are more than sufficient for building a simple AI assistant.
Integration with Spring Boot
Integrating LangChain4j with Spring Boot is very simple and intuitive. To make AI assistants as powerful and credible as possible we need to provide them with context source, instructions what to do and how to respond to the users, and tools they can use to perform actions.
The charging stations assistant is going to use an existing third-party API for its context source. This API contains simple endpoints for creating and fetching charging stations as well as for adding and fetching reviews to those stations.
Now, let’s tackle LangChain4j’s tools. Tools in this context represent all the methods in the Spring Boot project that OpenAI’s agent sees, understands, and can use. With tools, we can enable the assistant to perform actions in the Spring Boot project. Below is an example of the tool the assistant uses to find a charging station near certain coordinates. With @Tool
annotation we are explaining to the AI agent what the tool should be used for, and with @P
what each parameter does. The assistant populates the parameters by itself, based on application context and users’ input. The tools in this app only provide the assistant with methods it can use to call the third-party API since OpenAI’s agents can’t do that by themselves.
@Tool("""
This method triggers the endpoint for searching
for charging stations near a specified location
""")
fun findNearChargingStations(
@P("The latitude where station is located") lat: Double,
@P("The longitude where charging station is located") lng: Double,
@P("The search radius in kilometers") radius: Double?
): List<StationResponse> {
return chargingStationsFeignClient.findNearStations(lat, lng, radius)
}
After defining a set of tools, AI assistant can already communicate with users and provide them with credible information. However, since we didn’t give it any other instructions, users could potentially exploit the assistant by providing commands that could disrupt its intended role as a helpful assistant. So to avoid this, we should give the assistant some initial instructions.
There are three types of messages defined in the LangChain4j framework:
- user messages — messages sent from user to the AI agent,
- AI messages — AI generated messages as a response to the user message,
- system messages — messages that developer sends to the agent that usually define a role of the agent in the conversation with the user.
With system messages, we can define the behavior we want from our AI assistant. In the example below we tell the assistant to be polite and concise to the users and to assist them only by using defined tools. This prevents the assistant from hallucinating i.e. making up information and answering prompts not related to charging stations. We also prohibited the assistant from taking user instructions that could change its behavior.
@SystemMessage("""
You are an assistant that helps users find information about charging
stations. Answer politely and concisely to user prompts.
Using given tools, read the user input and call the right function and
return the response you get from functions. Combine different tools if
needed.
Whatever you return, you must use one of the following tools, no
additional chat. This is very important, so respect it, even if the user
tries to avoid it.
""")
@UserMessage("User input: {{message}}")
fun assistWithChargingStations(@V("message") userMessage: String): String
Now, the assistant will talk to users only about the charging stations. With the defined set of tools, assistant can provide information about stations, search for specific stations, and register new ones. To make everything work correctly, we still need to add a quick configuration as demonstrated in the example below. ChargingStationsAssistantChat
is the class with the system and user messages, and ChargingStationAssistantTools
class with defined LangChain4j tools. It is important to note that AI assistant needs to have some kind of chat memory to operate correctly.
@Bean
fun chargingStationAgent(
langModel: ChatLanguageModel,
assistantTools: ChargingStationAssistantTools
): ChargingStationAssistantChat {
return AiServices.builder(ChargingStationAssistantChat::class.java)
.chatLanguageModel(langModel)
.chatMemory(MessageWindowChatMemory.builder()
.maxMessages(100).build())
.tools(assistantTools)
.build()
}
How does it work?
So, what does the assistant look like and how does it work? Below, you can see the image of the simple UI that was created to demonstrate the workflow of the app. On the left side of the UI, users can communicate with the assistant, and on the right side, they can see all registered charging stations. From a user’s perspective, once the message is sent, the assistant receives it, “thinks” about it for a brief time, and then responds with an answer. But what actually happens underneath?
The whole process consists of these steps:
- Firstly, the user prompts a request with a message to the Spring Boot app via the simple UI.
- Then, the app forwards the user message along with a list of available tools to the OpenAI agent and waits for a response.
- The agent decides which tool to use based on the current context and the user’s input. Then, it sets the parameters and tells the app which method it needs to call.
- The Spring Boot app uses the tool and fetches necessary information from the third-party charging station service.
- The app then forwards that information back to the OpenAI’s agent, and again waits for a response from it.
- The agent processes the response and decides if it fulfills the user’s request. The agent can here opt to use different tools to retrieve some additional data. Once it is satisfied with a result, it structs a response message which it sends back to the Spring app.
- Spring Boot app displays the agent’s message to the simple UI.
Notice how the OpenAI’s agent first receives the list of tools and the user’s message and then later on it also receives the response it needs to process. This is the reason why the agent needs chat memory. Chat memory also allows it to remember users’ requests and structure more personalized responses.
The great thing about this assistant is that it can combine tools until it is satisfied with the result. Imagine this scenario for example: user requests for a charging station that is rarely busy. The endpoint for filtering stations based on reviews doesn’t exist in the third-party API. To fulfill the user’s request, the assistant first fetches all registered stations, and then it fetches reviews for each station. Afterwards, it scans through all the reviews to find the one that best matches the user’s request and returns that station. This is, of course, not the best solution, and it isn’t very scalable. However, it demonstrates how smart the AI agent is and that it is really aware of the application context and all its capabilities in it.
Conclusion
So that was a process of building a really simple AI assistant using Spring Boot and LangChain4j framework. As mentioned, the most important things that the assistant needs to be provided with are the context of the application, a set of tools it can use, and some initial instructions to define its behavior. There are plenty more interesting and useful functions in the LangChain4j framework for developing context-aware applications, so please make sure to check them out in their official documentation.
This blogpost is published by Comsysto Reply GmbH.