Building AI Ageants with Spring AI, Embabel and Amazon Bedrock AgentCore - Part 3

Building AI Agents with Embabel, Spring AI and Amazon Bedrock AgentCore

Building AI Ageants with Spring AI, Embabel and Amazon Bedrock AgentCore - Part 1

Building AI Agents with Embabel, Spring AI and Amazon Bedrock AgentCore – Part 1 Introduction to the series and to the Embabel framework

Building AI Ageants with Spring AI, Embabel and Amazon Bedrock AgentCore - Part 2

Building AI Agents with Embabel, Spring AI and Amazon Bedrock AgentCore – Part 2 Develop Agents with Embabel shell

Building AI Ageants with Spring AI, Embabel and Amazon Bedrock AgentCore - Part 3

Building AI Agents with Embabel, Spring AI and Amazon Bedrock AgentCore – Part 3 Develop Agents as web application with Embabel

Building AI Agents with Embabel, Spring AI and Amazon Bedrock AgentCore – Part 3 Develop Agents as web application with Embabel

Introduction

In part 2, we developed our first AI agents with the Embabel shell. We learned many concepts of this framework, such as agents, actions, and goals. In this article, we’ll slightly adjust our application to convert it into a web application.

Implement AI agents as a web application with Embabel

You can look at my GitHub embabel-1.x-conference-app-agent-local repository for the example. We’ll completely reuse the implementation of both agents, the CreateTalksAndApplyForConferencesAgent and the  SearchForTalksAndApplyForConferencesAgent that we implemented in part 2.

Those agents are capable of responding to the following prompts:

  1. “Please provide me with the list of conferences, including their IDs, with Java topics happening in 2027, with the call for papers open today. Also, provide me with the list of my talks with this topic in the title. Finally, for each conference and talk retrieved, apply individually for the conference.” SearchForTalksAndApplyForConferencesAgent is responsible for responding to this prompt.
  2. “Please create a talk with a cool title (max 60 characters long) and description (max 300 characters long) about using Spring AI on the Amazon Bedrock AgentCore service. Then provide me with the list of conferences, including their IDs, with Java topics happening in 2026 and 2027, with the call for papers open today. Finally, for each conference, apply individually for it with the talk just created.” CreateTalksAndApplyForConferencesAgent is responsible for responding to this prompt.

First, let’s check that we declared the following dependencies in pom.xml:

 <dependency>
    <groupId>com.embabel.agent</groupId>
    <artifactId>embabel-agent-starter-bedrock</artifactId>
    <version>${embabel-agent.version}</version>
 </dependency>
 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
 </dependency>

Also, make sure not to declare the embabel-agent-starter-shell dependency, as we don’t use Embabel shell in this article.

All we need to do now is to additionally implement the EmbabelAgentController controller.

Let’s first implement a generic endpoint that will accept both prompts. Let’s first autowire and create some objects coming from Embabel that we’ll use for this:

@RestController
public class EmbabelAgentController {

private final AgentPlatform agentPlatform;
private final AgentInvocation<Domain.ConferenceApplications> invocation;
	
private final ProcessOptions processOptions = new ProcessOptions()
     .withVerbosity(new Verbosity()
     .withShowPrompts(true)
     .withShowLlmResponses(true)
     .withShowPlanning(true)
     .withDebug(true));


public EmbabelAgentController(AgentPlatform agentPlatform) {
      this.agentPlatform = agentPlatform;
		
      this.invocation = AgentInvocation
        .builder(agentPlatform)
	    .options(processOptions)
	    .build(Domain.ConferenceApplications.class);
  }
...

}	

First, we autowire the Embabel’s AgentPlatform object and use it to create the AgentInvocation object. We also pass the ProcessOption object configured to show planning, LLM responses, and prompts. Also, we pass the type of the response – the domain object returned after the agents achieve their goals; in our case, the  Domain.ConferenceApplications.

Now, let’s implement the endpoint itself:

@GetMapping(value = "/generic", consumes = "text/plain")
public Domain.ConferenceApplications genericPrompt(@RequestParam String prompt) {
    var inputs = Map.of("request", new UserInput(prompt)); 
    return this.invocation.run(inputs).last(Domain.ConferenceApplications.class);	
}

Here we wrap the prompt into Embabel’s UserInput object that we put into the map with all inputs required to run the agent. Then we use the AgentInvocation object to run the agent with the specified input synchronously. We can also invoke runAsync to run the agent asynchronously. Finally, we retrieve the result of the agent invocation with the specified type.

Let’s build the application with mvn clean package and run it locally with mvn spring-boot: run. We’ll send the prompt number 1 to this endpoint. What we’ll see is that Embabel picked the wrong agent, CreateTalksAndApplyForConferencesAgent, to run. Why? As we don’t use the Embabel shell, Embabel doesn’t analyze the prompt and compute the score to determine what exact agent to run. It currently only looks into the return type of all agents and picks the first one that matches the type defined in the AgentInvocation object. But both our agents have the same return type: Domain.ConferenceApplications. Their goals, in the end, are the same: to apply for the conferences. With that, for our 2 agents, there is a 50% probability that Embabel will pick the wrong agent to run.

How can we fix that? For example, we can define the endpoint per agent. Let’s do this:

@GetMapping(value = "/applyToConferencesWithExistingTalks", consumes = "text/plain")
public Domain.ConferenceApplications applyToConferencesWithExistingTalks(@RequestParam String prompt) {
    return this.invokeAgent(prompt,
       SearchForTalksAndApplyForConferencesAgent.AGENT_NAME);	
}
	
@GetMapping(value = "/applyToConferencesWithNewTalks", consumes = "text/plain")
public Domain.ConferenceApplications applyToConferencesWithNewTalks(@RequestParam String prompt) {
    return this.invokeAgent(prompt,
        CreateTalksAndApplyForConferencesAgent.AGENT_NAME);	
}

I additionally wrote 2 helper methods, invokeAgent and getAgentByName, to simplify the agent search by name and its subsequent invocation:

private Domain.ConferenceApplications invokeAgent(String prompt, String agentName) {		
     var inputs = Map.of("request", new UserInput(prompt));
     var agent = this.getAgentByName(agentName);
     var agentProcess = this.agentPlatform.createAgentProcess(agent,
                  processOptions, inputs);
     return agentProcess.run().last(Domain.ConferenceApplications.class);
}
	
private Agent getAgentByName(String agentName) {
     var optionalAgent = this.agentPlatform.agents()
	    .stream()
	    .filter(a -> a.getName().equals(agentName))
	    .findFirst();
	   
     if( optionalAgent.isEmpty()) {
	   throw new RuntimeException("agent with the name "+agentName+ " not found");
     }
	   
    return optionalAgent.get();
}

Let’s explain what happens in the invokeAgent method. We first invoke the getAgentByName method. It searches in the collection of agents that the agent platform knows for one that has a specified name. All classes annotated with Embabel’s @Agent annotation will be registered in the agent platform. Then we use the AgentPlatform object to create the AgentProcess and pass the found agent, the already described process options, and inputs to it. Then we run the AgentProcess and return its last output of type Domain.ConferenceApplications.

Now we send prompt number 1 (see above) to the applyToConferencesWithExistingTalks endpoint and prompt number 2 to the applyToConferencesWithNewTalks endpoint. We’ll see something similar as a result, as we saw in part 2.

For example, here I use httpie to send prompt number 1:

http GET http://localhost:8080/applyToConferencesWithExistingTalks?prompt="Please provide me with 
the list of conferences including their IDs with Java topic happening in 2026 and 2027 with 
call for papers open today. Also provide me with the list of my talks with this topic in the title. 
Finally, for each conference and talk retrieved, apply individually for the conference." 
Content-Type:text/plain`

Here is the response of the agent:

Agent response -2

Having one endpoint per Embabel agent has its pros and cons. The biggest disadvantage is that a service like AgentCore Runtime exposes only one HTTP POST /invocations endpoint to send the prompt to the web application. This means that we’ll need to think about how to design, implement, and deploy each Embabel agent on a separate AgentCore Runtime. Each agent belonging to the same web application will have a lot of shared logic with other agents. This is not very convenient. The biggest advantage is that you can scale each Embabel agent individually. AgentCore Runtime will do it for you.

It’s worth checking for the upcoming version of Embabel whether there are other options available to invoke the agent.

Conclusion

In this article, we covered how to develop a web application with Embabel.

If you like my content, please follow me on GitHub and give my repositories a star!

Building AI Agents with Embabel, Spring AI and Amazon Bedrock AgentCore

Building AI Agents with Embabel, Spring AI and Amazon Bedrock AgentCore – Part 2 Develop Agents with Embabel shell