Skip to content

CHAT APP: Connection Angular Client to Google ADK

Connecting an Angular Client to a Google ADK Agent Service

This outlines the standalone code which connects an Angular Material client to a JAVA backend to a RESTful service which uses the Google Agent Development Kit to define an Large Language Model Agent. The prompt from the Angular Material client is sent to the JAVA backend, via a RESTful service. The service then runs an Agent which interprets the prompt and sends an answer back to the client.

This work sets up a prototype representing the very first step in developing a complete implementation where an Angular client is connected to an Agent in the background.It is a simple client which sets up an Angular chat window where the prompt is given by the user and the response of the agent is displayed. When the prompt is given and the send button is pressed, the Angular code sends the prompt to the RESTful service via the angular HttpClient (the endpoint in this project is ‘/api/agent/simple’. On the JAVA side, a WebServlet class has been set up and the doPost method extracts the prompt and sets up a Runner to access the agent. The result of the method sends the agent response back to the client which displays it in the chat window.

In this prototype, the Agent, ScienceTeacherAgent,  is a simple agent having one task (defined in the instructions of the task): You are a science teacher, answer the question with simple language.

The agent is defined within a JAVA framework using ADK. A session is created by the backend service  and the Agent is asynchronously run using the prompt from the client. The final response from the agent is then send back to the client. The client then displays the result in a chat window.

 

General Code Details

Application Structure

A (Eclipse) Maven project, JavaAIAgent,  was created that contains both the JAVA backend code and the Angular Material code.

JAVA code structure

Under the JavaAIAgent is the information for a typical maven project

  • /src/main/java: The root directory of the JAVA packages (/src/main/java is the test classes, but are not used in this application)
  • pom.xml: The maven project definition

When run this backend service runs on localhost:8080.

Angular code Structure

The Angular code is found under JavaAIAgent/frontend. And the directories under the is that of an Angular client application. The package structure starts from frontend/src/app. 

When run, this client runs on http://localhost:4200. The angular code is compiled in the frontend directory and the 

General Flow

The general flow of the execution is as follows:

  • (simple-chat component): The user enters a prompt to be given to the agent
  • (agent service): When the user issues ‘send’, then the agent service makes a call to the http endpoint (/api/agent/simple) using HttpClient. The prompt, sessionId and userId are sent to the service (http.post<AgentJsonResponse>)
  • The JAVA servlet at /api/agent/simple  (SimpleLLMServlet)  receives the prompt, sessionId and userId creates a session (InMemorySessionService) and a runner (Runner) with the name of the top agent (ScienceTeacherAgent), The prompt is placed the content (Content) and the runAsync command executed with the runner creating a list of events (Iterable<Event>).
  • From the event of the final response (event.finalResponse()) the response text is isolated and sent back in an AgentJsonResponse structure (this has the response, the sessionId and the status, SUCCESS).
  • The response from the service is received as an Observable in the client angular code (the return of the http.post<AgentJsonResponse> command).
  • In the chat service, the new response is added to the list of responses and this show up in the chat window.

Sample Run

Chat window of Agent client

Client Code

The client is written in Angular Material. The critical components and services are:

  • chat/simple-chat (SimpleChat): The component that governs the input of the prompt and the displaying of the responses.
  • services/agent (AgentService): This code makes the post to the server (sendMessage) and manages the list of responses (messageList)
  • models/chat-model (ChatMessage): This holds the format of the messages, with role (whether from the user or from the agent) and the text response.

 

SimpleChat: typescript

📘
simplechat.ts
import { CommonModule } from '@angular/common';
import { Component, ElementRef, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { AgentService } from '../../services/agent';
@Component({
  selector: 'app-simple-chat',
  standalone: true,
  imports: [
    CommonModule, FormsModule, MatCardModule,
    MatFormFieldModule, MatInputModule, MatButtonModule, MatIconModule
  ],
  templateUrl: './simple-chat.html',
  styleUrl: './simple-chat.scss',
})
export class SimpleChat {
	userInput = ''; 
	
	// For simplicity, using a fixed session and user ID here
	currentSessionId = 'session123';
	userID = 'user1';
	
	// this list manipulation is done through the AgentService
	constructor(public agentService: AgentService) {}
	
	onSend() { 
		// userInput comes from the input field bound with [(ngModel)]
	  const textToSend = this.userInput;
	  if (!textToSend.trim()) return;
	  
	  this.userInput = ''; // Reset the input field
	  
	  // Add the user's message to the UI immediately through the service
	  this.agentService.addMessage({ role: 'user', text: textToSend });
	// Send the message to the backend to get a response and add it to the chat
	  this.agentService.sendMessage(textToSend, this.userID, this.currentSessionId).subscribe({
	    next: (res) => {
	        this.agentService.addMessage({ role: 'assistant', text: res.message });
	    },
	    error: (err) => console.error('Communication failed', err)
	  });
	}
}

SimpleChat: html

The chat window is contained within a mat-card.

The header, mat-card-header, holds the title and a button to clear the chat (agentService.clearChat())

The card content (mat-card-content) displays the messages by looping over the list of messages: <div *ngFor=“let msg of agentService.messages()”>

How each message is displayed is determined by whether it is from the user (SSCS class user-bubble) or from the assistent (SCSS class assistent-bubble) . This is set by <div [ngClass]=“msg.role + ‘-bubble'”>

The prompt message is setup by <input matInput [(ngModel)]=“userInput” (keyup.enter)=“onSend()”>

And the button to send the message to onSend() is defined with

<button mat-icon-button matSuffix (click)=“onSend()”>

<mat-icon>send</mat-icon>

 

SimpleChat: style

🌐
simple-chat.html
<mat-card class="chat-container">
  <mat-card-header>
    <mat-card-title>Simple LLM Physics Agent</mat-card-title>
    <span class="spacer"></span>
    <button mat-icon-button (click)="agentService.clearChat()">
      <mat-icon>delete_sweep</mat-icon>
    </button>
  </mat-card-header>
  <mat-card-content #scrollContainer class="message-area">
    <div *ngFor="let msg of agentService.messages()">
      <div [ngClass]="msg.role + '-bubble'">
        {{ msg.text }}
      </div>
    </div>
  </mat-card-content>
  <mat-card-actions class="input-area">
    <mat-form-field appearance="outline" class="full-width">
      <mat-label>Ask a physics question...</mat-label>
      <input matInput [(ngModel)]="userInput" (keyup.enter)="onSend()">
      
      <button mat-icon-button matSuffix (click)="onSend()">
        <mat-icon>send</mat-icon>
      </button>
    </mat-form-field>
  </mat-card-actions>
  
</mat-card>
🎨
simple-chat.scss
.chat-container {
  max-width: 800px;
  margin: 20px auto;
  height: 80vh; /* Give the card a fixed height */
  display: flex;
  flex-direction: column;
}
.spacer { flex: 1 1 auto; }
.message-area {
   height: 400px;
   overflow-y: auto;
   display: flex;
   flex-direction: column;
   padding: 16px;
 }
 .input-area {
    padding: 0 16px 16px 16px;
    display: block; /* Ensures the form-field takes full width */
  }
  .full-width {
    width: 100%;
  }
  .user-bubble {
    align-self: flex-end;
    background-color: #e3f2fd;
    padding: 8px 12px;
    border-radius: 15px 15px 0 15px;
    margin-bottom: 8px;
    max-width: 80%;
  }
  /* User on the right, blueish */
  .assistant-bubble {
    align-self: flex-start;
    background-color: #f5f5f5;
    padding: 8px 12px;
    border-radius: 15px 15px 15px 0;
    margin-bottom: 8px;
    max-width: 80%;
  }

JAVA Backend

The JAVA backend is a RESTful service which uses the JAVA Agent Development Kit (ADK) to set up a Runner class which is used to asynchronously run defined agent and get the response (as events) where the ‘final’ event is the response of the agent. 

The agent

filename.java
package esblurock.info.JavaAIAgent.science;
import java.util.List;
import com.google.adk.agents.BaseAgent;
import com.google.adk.agents.LlmAgent;
import esblurock.info.JavaAIAgent.Constants;
import esblurock.info.JavaAIAgent.science.biology.BiologyAgent;
import esblurock.info.JavaAIAgent.science.physics.PhysicsAgent;
/** Science teacher agent. */
public class ScienceTeacherAgent {
    public static final BaseAgent ROOT_AGENT = LlmAgent.builder()
            .name(Constants.APPNAME)
            .description("Head Science Teacher")
            .instruction("""
                You are a science teacher, answer the question with simple language. 
                """)
            .model(Constants.MODEL)
            .build();
}

The Servlet

The WebServlet is defined as the SimpleLLMServlet JAVA class with a doPost method that sets up and runs the agent.

The major ADK classes use to run the agent (in order of appearance) are:

  • ConcurrentMap<String, Object> initialState: Initializes the state to an empty list (used in the Runner builder)
  • InMemorySessionService sessionService: This is where the ADK session is stored. This class is used primarily for developing and prototype purposes in that if the session restarts, the session is lost. For production purposes, other, more persistent, classes are available. 
  • sessionService.createSession: Here the session is actually created. Note that a session is defined by the userId, sessionId and the app-name.
  • Runner runner = Runner.builder(): This sets up the class that is used to actually run the (top) agent. In this project the top agent is ScienceTeacherAgent (see definition below). The session that was just defined is part of the build.
  • Content newMessage = Content.builder(): This is the class that holds the prompt information. 
  • Content newMessage = Content.builder(): The runner is now run (asynchronously) with the content having the prompt. The result is a list of responses in Events. 
  • processEvents(events, sessionId, resp); This is a class method which loops over all the events to find the response.
  • if (event.finalResponse()): The final response is alway the final result of the response of the agent. If multiple agents or functions are involved (not in this project), other events would hold this information. 
  • mapper.writeValue(resp.getWriter(): This passes the result (as a stream) to the client
  • AgentJsonResponse(“SUCCESS”, text, sessionId): The result is packaged in a JSON object.
SimpleLLMServlet.java
package esblurock.info.JavaAIAgent.background;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.adk.events.Event;
import com.google.adk.runner.Runner;
import com.google.adk.sessions.InMemorySessionService;
import com.google.genai.types.Content;
import com.google.genai.types.Part;
import esblurock.info.JavaAIAgent.Constants;
import esblurock.info.JavaAIAgent.dto.AgentJsonResponse;
import esblurock.info.JavaAIAgent.science.ScienceTeacherAgent;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@WebServlet("/api/agent/simple")
public class SimpleLLMServlet extends HttpServlet {
	
    private static final long serialVersionUID = 1L;
	ObjectMapper mapper = new ObjectMapper();
	
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.setContentType("application/json");
        resp.setCharacterEncoding("UTF-8");
        // Set CORS for Angular (Change localhost:4200 to your actual frontend URL)
        resp.setHeader("Access-Control-Allow-Origin", "http://localhost:4200");
        resp.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
        resp.setHeader("Access-Control-Allow-Headers", "Content-Type");
        TypeReference<Map<String, Object>> mapType = new TypeReference<>() {};
        Map<String, Object> body = mapper.readValue(req.getReader(), mapType);
        String userId = (String) body.get("userId");
        String sessionId = (String) body.get("sessionId");
        String prompt = (String) body.get("prompt");
        
        // set up a seession service with the current session and user ID
        ConcurrentMap<String, Object> initialState = new ConcurrentHashMap<String, Object>();        
        InMemorySessionService sessionService = new InMemorySessionService();
        sessionService.createSession(
        		Constants.APPNAME,     // App Name
        	    userId,                // User ID
        	    initialState,          // Initial state (Map<String, Object>)
        	    sessionId              // Wrap your String ID in an Optional
        	).blockingGet();           // Wait for completion/ Use blockingGet() to ensure it's created before the next line
        // Create the runner with the top-level agent and session service
        Runner runner = Runner.builder()
                .appName(Constants.APPNAME)
                .agent(ScienceTeacherAgent.ROOT_AGENT)
                .sessionService(sessionService) // <--- Tell the runner where to look
                .build();
        // set up content with the prompt
        Content newMessage = Content.builder()
                .role("user") // <--- Some versions REQUIRE the role to be set to "user"
                .parts(List.of(Part.builder().text(prompt).build()))
                .build();
        // run the agent asynchronously
        Iterable<Event> events = runner.runAsync(
        	    userId, 
        	    sessionId, 
        	    newMessage // <--- Ensure this object actually contains the text
        	).blockingIterable();
        // We put the processing in a separate method for clarity
       	processEvents(events, sessionId, resp);
       
    }
/*
 * Process the events from the agent run
 * Loop through the events
 * 
 * Since this is a simple example agent (there are no subagents or , we just look for final responses
 * Technically there could be multiple events (with subagents or functions, but here we just expect one final response)
 * 
 */
	private void processEvents(Iterable<Event> events, String sessionId, HttpServletResponse resp) {
    	try {
        for (Event event : events) {
            // 1. Check if it's the final answer
            if (event.finalResponse()) {
                 // stringifyContent() is the library's built-in way to turn the 
                // complex Content object into a simple String for your UI.
                String text = event.stringifyContent();
                mapper.writeValue(resp.getWriter(), new AgentJsonResponse("SUCCESS", text, sessionId));
                return;
            } else {
                String text = event.stringifyContent();
                mapper.writeValue(resp.getWriter(), new AgentJsonResponse("SUCCESS", text, sessionId));          	
            }
        }
    	} catch (Exception e) {
    		e.printStackTrace();    	}		
	}
}

Application Main Routine

This is the setup for the backend service. In a production enviroment or a larger service, this main routine would also set up and initializing other tools such as databases or ontologies (as with ChemConnect).

For this project some of the critical points are:

  • Server server = new Server(8080); This sets up localhost:8080 as the endpoint of the service
  • WebAppContext webapp = new WebAppContext(); This is establishing all the configurations and the WebServlets 
  • ContainerIncludeJarPattern This attribute WebAppContext sets up the search for all the WebServlets that have been defined (with the annotation).
  • server.setHandler(webapp); Sets the configuration for the server.
AgentApplication.java
package esblurock.info.JavaAIAgent;
import java.io.File;
import org.eclipse.jetty.annotations.AnnotationConfiguration;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.JettyWebXmlConfiguration;
import org.eclipse.jetty.webapp.MetaInfConfiguration;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.webapp.WebInfConfiguration;
import org.eclipse.jetty.webapp.WebXmlConfiguration;
import org.eclipse.jetty.servlet.ServletHolder;
import esblurock.info.JavaAIAgent.background.SimpleLLMServlet;
public class AgentApplication {
    public static void main(String[] args) throws Exception {
        Server server = new Server(8080);
        WebAppContext webapp = new WebAppContext();
        webapp.setContextPath("/");
        webapp.setResourceBase("src/main/webapp");
        webapp.setDefaultsDescriptor(null);
        // 2. Standard Configurations for Jetty 11 Jakarta scanning
        webapp.setConfigurations(new Configuration[] {
            new AnnotationConfiguration(),
            new WebXmlConfiguration(),
            new WebInfConfiguration(),
            new JettyWebXmlConfiguration(),
            new MetaInfConfiguration()
        });
        webapp.setExtraClasspath("target/classes");
        webapp.setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern", 
            ".*/target/classes/.*|.*[\\\\/]target[\\\\/]classes[\\\\/].*");
        server.setHandler(webapp);
        System.out.println("Starting Agent Server with Annotation Discovery...");
        server.start();
        server.join();
    }
}

Maven pom.xml

The backend is setup as a maven application.  Crucial for the setup of the Google Agent Development Kit (ADK) are the following dependencies (google-adk-dev is not essential):

<dependencies>
<dependency>
<groupId>com.google.adk</groupId>
<artifactId>google-adk-dev</artifactId>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>com.google.adk</groupId>
<artifactId>google-adk</artifactId>
<version>0.5.0</version>
</dependency>


Using the exec-maven-plugin plugin, the following command starts the backend:

mvn clean compile exec:java 
📄
pom.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>esblurock.info</groupId>
	<artifactId>JavaAIAgent</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>JavaAIAgent</name>
	<url>http://www.esblurock.info</url>
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.release>17</maven.compiler.release>
		<adk.source-dir>src/main/java</adk.source-dir>
		<adk.package-name>esblurock.info.JavaAIAgent.agents</adk.package-name>
		<adk.port>8080</adk.port>
	</properties>
	<profiles>
		<profile>
			<id>science</id>
			<properties>
				<adk.source-dir>.</adk.source-dir>
				<adk.package-name>esblurock.info.JavaAIAgent.science</adk.package-name>
			</properties>
		</profile>
		<profile>
			<id>transaction</id>
			<properties>
				<adk.source-dir>.</adk.source-dir>
				<adk.package-name>esblurock.info.JavaAIAgent.transactions</adk.package-name>
				<adk.port>9000</adk.port>
			</properties>
		</profile>
	</profiles>
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.junit</groupId>
				<artifactId>junit-bom</artifactId>
				<version>5.11.0</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>com.google.adk</groupId>
			<artifactId>google-adk-dev</artifactId>
			<version>0.5.0</version>
		</dependency>
		<dependency>
			<groupId>com.google.adk</groupId>
			<artifactId>google-adk</artifactId>
			<version>0.5.0</version>
		</dependency>
<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-annotations</artifactId>
    <version>11.0.24</version>
</dependency>
		<dependency>
			<groupId>jakarta.servlet</groupId>
			<artifactId>jakarta.servlet-api</artifactId>
			<version>6.0.0</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
			<version>2.18.0</version>
		</dependency>
		<dependency>
			<groupId>org.junit.jupiter</groupId>
			<artifactId>junit-jupiter-api</artifactId>
			<scope>test</scope>
		</dependency>
		<!-- Optionally: parameterized tests support -->
		<dependency>
			<groupId>org.junit.jupiter</groupId>
			<artifactId>junit-jupiter-params</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<build>
		<pluginManagement><!-- lock down plugins versions to avoid using Maven 
				defaults (may be moved to parent pom) -->
			<plugins>
				<!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
				<plugin>
					<groupId>org.codehaus.mojo</groupId>
					<artifactId>exec-maven-plugin</artifactId>
					<version>3.5.0</version>
					<configuration>
						<mainClass>esblurock.info.JavaAIAgent.AgentApplication</mainClass>
						<classpathScope>compile</classpathScope>
						<arguments>
							<argument>--adk.agents.source-dir=${adk.source-dir}</argument>
							<argument>--adk.agents.package=${adk.package-name}</argument>
							<argument>--server.port=${adk.port}</argument>
						</arguments>
					</configuration>
				</plugin>
				<plugin>
					<artifactId>maven-clean-plugin</artifactId>
					<version>3.4.0</version>
				</plugin>
				<!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
				<plugin>
					<artifactId>maven-resources-plugin</artifactId>
					<version>3.3.1</version>
				</plugin>
				<plugin>
					<artifactId>maven-compiler-plugin</artifactId>
					<version>3.13.0</version>
				</plugin>
				<plugin>
					<artifactId>maven-surefire-plugin</artifactId>
					<version>3.3.0</version>
				</plugin>
				<plugin>
					<artifactId>maven-jar-plugin</artifactId>
					<version>3.4.2</version>
				</plugin>
				<plugin>
					<artifactId>maven-install-plugin</artifactId>
					<version>3.1.2</version>
				</plugin>
				<plugin>
					<artifactId>maven-deploy-plugin</artifactId>
					<version>3.1.2</version>
				</plugin>
				<!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
				<plugin>
					<artifactId>maven-site-plugin</artifactId>
					<version>3.12.1</version>
				</plugin>
				<plugin>
					<artifactId>maven-project-info-reports-plugin</artifactId>
					<version>3.6.1</version>
				</plugin>
			</plugins>
		</pluginManagement>
	</build>
</project>

Leave a Reply

Your email address will not be published. Required fields are marked *

en_USEnglish