Skip to content

Generic Code connecting Angular client to JAVA Google ADK Agent backend

The interface connecting an Angular client and the JAVA backend server connected to a Google ADK Agent applications, as found in previous blog posts (1, and 2), follows basically the same pattern.

In the backend, the only difference is the endpoint, the root or top agent to be used and the application name. All the other code is exactly the same. 

On the client side, the service agent calling the code is the same. The only difference is the creation of the body of the message to be sent to the backend and then how to interpret the Observable of the response. The body of the message can be created anywhere in the client code. This is sent to a centralized method, meaning the only place where the agent service is called, and it is in this centralized method that the response is interpreted.

Generic Agent Servlet Code

The generic code is essentially the same as the servelt code in a previous post ( section ‘The Servlet’ under ‘Java Backend’) . The differences lie in where the appname(applicationName)  and the Agent (topAgent) is needed. For a simple implementation, topAgent points to the agent. Under a mult-agent implementation the topAgent points to coordinator agent, from which all the other agents are called.

The application name is first used in the creation of a session:

sessionService.createSession(this.applicationName, // 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

Both the application name (applicationName) and the agent (topAgent) are needed in the creation of the Runner:

Runner runner = Runner.builder()

.appName(this.applicationName)

.agent(this.topAgent)

.sessionService(sessionService) // <— Tell the runner where to look

.build();

The agent and the application name are given in the creation of the instance of the servlet. For example, in the definition of the ‘human in the loop’ agent (within the HITL post), the SimpleHITLServlet class is defined as follows:

@WebServlet(“/api/agent/simplehitl”)

public class SimpleHITLServlet extends SimpleGenericServlet {

private static final long serialVersionUID = 4749598458122810112L;

public SimpleHITLServlet() {

super(Constants.HITLAPPNAME, SimpleHumanInTheLoop.COORDINATOR);

}}

 

SimpleGenericServlet.java
package esblurock.info.JavaAIAgent.generic;
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.agents.LlmAgent;
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 jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public class SimpleGenericServlet  extends HttpServlet {
	private static final long serialVersionUID = 1L;
	ObjectMapper mapper = new ObjectMapper();	
	
	private static String applicationName = "";
	private static LlmAgent topAgent = null;
	
	public SimpleGenericServlet(String appName, LlmAgent topagent) {
        applicationName = appName;
        topAgent = topagent;
    }
	
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
		System.out.println("Entered SimpleHITLServlet doPost");
		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 promptString = mapper.writeValueAsString(body);
		System.out.println("SimpleLLMServlet doPost: " + promptString);
		// 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(this.applicationName, // 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(this.applicationName).agent(this.topAgent)
				.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(promptString).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);
	}
	private void processEvents(Iterable<Event> events, String sessionId, HttpServletResponse resp) {
		try {
			for (Event event : events) {
				System.out.println("Event Type: " + event.stringifyContent());
				// 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();
					System.out.println("----------------------------------------------------------");
					System.out.println("Final Event: " + text);
					mapper.writeValue(resp.getWriter(), text);
					return;
				} else {
					String text = event.stringifyContent();
					System.out.println("----------------------------------------------------------");
					System.out.println("Intermediate Event: " + text);
					// mapper.writeValue(resp.getWriter(), new AgentJsonResponse("SUCCESS", text,
					// sessionId));
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println("DONE   ----------------------------------------------------------");
	}
	
}

JAVA main code

If no other initialization is needed of other tools (for example, initialization of the ontology or the database), the main code is essentually always the same. It is the job of the WebAppContext to discover all the defined servlets within the project. The endpoints of the servlets do not occur within the main code.

AgentApplication.java
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();
	}
}

Client Service for Calling Backend

In the client typescript code, the service calling the backend (and managing the messages) is essentually the same for all applications. The application specific code is done by the component which calls the service. 

The call to the backend is done with the command:

sendMessage(body: any): Observable<any> {

return this.http.post<any>(‘/api/agent/simplehitl’, body, {

headers: { ‘Content-Type’: ‘application/json’ }

})

}

Service Usage

The philosophy of the service usage is that anywhere in the client code, the body to be sent to the backend agent is created. This could be the result of buttons, prompts or events. It does not matter from which component the body comes. 

In contrast, the interpretation of the backend response is centralized with the call to the service and the subscription to the Observable which is the response. It is here that the response is interpreted and dealt with. This could include routing to other components or setting key variables.

The calling component sets up the body (a JsonObject) to be sent to the backend. The calling component then subscribes to the result. For example in the HITL code

, the body is created with the appropriate status:

const body = {

status: ‘new’,

task: prompt,

data: data,

sessionId: this.agentService.getSessionId(),

userId: this.agentService.getUserId()

};

The call is done centrally by the main chat component. It is here that the response is interpreted and dealt with. The return of the call from the agent is an Observable, which is subscribed to. In the HITL. code, the two types of status values are possible, ‘new’ and ‘use’:

this.agentService.sendMessage(body).subscribe({

next: (res) => {

try {

const data = JSON.parse(res);

const status = data.status;

if (status === ‘GetParameters’) {

} else if (status === ‘answer’) {

}

} catch (e) {

console.error(‘Error parsing response’, e);

}

},

error: (err) => console.error(‘Communication failed’, err)

});;

The response could be handled with a if-then-else loop (as in this example) or a switch.

📘
filename.ts
import { HttpClient } from '@angular/common/http';
import { effect, Injectable, signal } from '@angular/core';
import { ChatMessage } from '../models/chat-model';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
@Injectable({
  providedIn: 'root',
})
export class AgentHITL {
	// List of messages stored as a signal
	private messageList = signal<any[]>(this.loadMessages());
	messages = this.messageList.asReadonly();
	// storage of session ID
	
	userId: string = '';
	sessionId: string = '';
	
	constructor(private http: HttpClient, private router: Router) {
	}
	
	setUserAndSession() {
		this.userId = 'user123';
		this.sessionId = new Date().getTime().toString();
	}
	public getSessionId(): string {
		return this.sessionId;
		}
	public getUserId(): string {
		return this.userId;
		}
	// Retrieve messages from sessionStorage
	// Note that the session is updated by the effect defined in the constructor
	private loadMessages(): ChatMessage[] {
		const saved = sessionStorage.getItem('agent_messages');
		return saved ? JSON.parse(saved) : [];
	}
	// This is called to clear the chat history (from session and the list itself)
	clearChat() {
		sessionStorage.clear();
		this.messageList.set([]);
	}
	// This is the main function to send a message to the backend agent API
	// For this simple chat the endpoint is /api/agent/simple
	sendMessage(body: any): Observable<any>  {
		return this.http.post<any>('/api/agent/simplehitl', body, {
		  headers: { 'Content-Type': 'application/json' }
		})
	}
	// Add a message to the list (this is called by the chat  component)
	addMessage(msg: any) {
	  this.messageList.update(current => [...current, msg]);
	}
}

Leave a Reply

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

en_USEnglish