Friday, October 10, 2025

OpenAI Travel Agent example with Embabel

# Intro

Intro

Recently OpenAI presented its new product: an Agent Builder. They revealed that in the keynote of the OpenAI DevDay 2025 that is happening in San Francisco.

To show the capabilities of the Agent Builder they created a workflow that uses 2 agents to provide information about the sessions of the DevDay. They used files to provide context to one of the agents to be able to know the sessions occurring in the DevDay and also a Widget to show information directly in the ChatGPT user interface.

Later the same day OpenAI released a youtube video showing another demo of the Agent Builder “Intro to Agent Builder” (https://www.youtube.com/watch?v=44eFf-tRiSg). This time the video demo a AI workflow for a Travel Agent that classifies the user input to determine if the user wants an itinerary suggestion or if they are looking for a flight to travel.

If you are a Java or Kotlin developer you can do that right now with Embabel.

Building the TravelAgent

Find the example project here abadongutierrez/openai-agent-demo-with-embabel

Agent class

To create an agent with Embabel you need to annotate your class with annotation @Agent. In the example code there is class OpenAITravelAgent:

@Agent(
    description = "Help the user to generate a Itinerary or find a flight"
)
public class OpenAITravelAgent { ... }

This will tell the Embabel engine that this class contains an agent. When running the application within Embabel there is a mechanism that selects which agent can attend the user based on its description, an LLM is used here to select the agent.

Agent Actions

An action is where the agents do their work. Actions are methods annotated with @Action within the agent class.

Everything within Embabel is strongly typed so actions can receive and return your classes. This is amazing because we can easily refactor when needed.

The types the actions receives as arguments indicates its preconditions and the type the action returns indicate its postconditions. This is very important because in order to determine which action to execute Embabel uses a planning step after the execution of each action (this is done with a non-LLM AI algorithm that probably those that know something about game development are familiar with). This planning feature enable Embabel agents to perform tasks that go beyond sequential execution because the next step is determined by combining the known steps and the state of the agent world (values, conditions, preconditions that the agent tracks).

Classify user input

The first action to execute is that method where their only condition is to receive the UserInput to start the flow. That method is classify in the OpenAITravelAgent class:

@Action(description = "Classify the user input")
Option classify(UserInput userInput, OperationContext context) {
    Option option = context.ai()
        .withAutoLlm()
        .createObject(String.format("""
            You are a helpful travel assistant for classifying whether a message is about an itinerary or a flight

            # User input
            %s
            """,
            userInput.getContent()
        ).trim(), Option.class);

    return option;
}

This method also receives the OperationContext of the agent flow that is executed. This class gives us the ability to execute AI operations with LLMs (among other things). In this case, we are asking an LLM (the default LLM) to analyze the user input and classify if the user wants an suggestion for an itinerary or a flight.

But we aren’t asking for a simple string as a result, we want the result to be a enumeration of type Option which could be ITINERARY or FLIGHT.

Deciding the kind of suggestion

Once the agent knows what the users needs the next action to execute is decide and this is because the precondition of the action is to have the UserInput and the Option.

@Action
ItineraryOrFlightRequest decide(UserInput userInput, OperationContext context, Option option) {
    if (option == Option.ITINERARY) {
        return new ItineraryOrFlightRequest(new ItineraryRequest(userInput), null);
    } else {
        return new ItineraryOrFlightRequest(null, new FlightRequest(userInput));
    }
}

This action basically decides which kind of request the agent is going to attend a ItineraryRequest or a FlightRequest and to do that the request type is wrapped in a class ItineraryOrFlightRequest which is a class that implements interface SomeOf.

Types implementing the interface SomeOf basically tells the Embabel that, when planning the next step, some sort of “switch” (or if-else) needs to be executed to decide the next action in the flow.

Creating the Itinerary or looking for a Flight

Whether the user is requesting an itinerary or looking for a flight actions itineraryAction or flightAction are executed:

@Action(description = "Build a travel itinerary based on user input")
Itinerary itineraryAction(ItineraryRequest request, OperationContext context) {
    Itinerary itinerary = context.ai()
        .withAutoLlm()
        .createObject(String.format("""
            You are a travel assistant, so build a concise itinerary based on the user input

            Today is: %s
            User input: %s
            """,
            LocalDateTime.now(),
            request.userInput().getContent()
        ).trim(), Itinerary.class);
    return itinerary;
}

@Action(description = "Find a flight based on user input")
Flight flightAction(FlightRequest request, OperationContext context) {
    Flight flight = context.ai()
        .withAutoLlm()
        .createObject(String.format("""
            You are a travel assistant. Always recommend a specific flight to go to. Use airport codes.

            Today is: %s
            User input: %s
            """,
            LocalDateTime.now(),
            request.userInput().getContent()
        ).trim(), Flight.class);
    return flight;
}

Both actions receive the request type (ItineraryRequest or FlightRequest) and, using a LLM create an Itinerary or a Flight to give that as a recommendation to the user.

In this code example I am using a API key from OpenAI so when asking something to their LLM if the LLM determine it needs to search the web to get extra info it will do it, in this case the LLM is doing that to get me a real flight with its number, the departure and destination as airport codes and the date.

But if I were using some local LLM there are ways to indicate the local LLM to use some internal tools to help it find the information requested, for example:

context.ai()
  .withDefaultLlm()
  .withToolGroup(CoreToolGroups.WEB) // Use the Web as tool!
  .createObject(...)

Fulfilling the goal

So once we have an Itinerary of a Flight the next step is basically tell the user the final recommendation. That happens in actions provideTravelRecommendationAsItinerary or provideTravelRecommendationAsFlight that basically create a text description to show to the user.

@AchievesGoal(
    description = "The user has received a travel itinerary"
)
@Action
Recomendation provideTravelRecommendationAsItinerary(Itinerary recommendable) {
    return new Recomendation(recommendable.recommendation());
}

@AchievesGoal(
    description = "The user has received a flight recommendation"
)
@Action
Recomendation provideTravelRecommendationAsFlight(Flight recommendable) {
    return new Recomendation(recommandable.recommendation());
}

It is important to notice here that these final actions are annotated with @AchievesGoal which basically indicates that these are the final steps for the agent and that executing them achieves the goal indicated in the description.

Defining at least one goal for the agent is important because otherwise the agent does know when to stop.

Running the agent

Before running the application you need to have an api key from OpenAI and setup the environmental variable OPENAI_API_KEY in your terminal.

export OPENAI_API_KEY=XXXXXXXXXXXX

Note: If you want to try modifying the code to run this little app using local LLM with Ollama, you will need some modifications like I have in the repo I used for the following post: https://abaddon-gtz.blogspot.com/2025/07/a-simple-agent-to-summarize-web-content.html

To run the application you can execute the “shell” script in “scripts” folder directly in the root of the project:

> ./scripts/shell.sh

This will start the Spring Shell application and it will give you a prompt where you can run commands.

To run the agent and get an itinerary suggestion you could execute in the spring shell the following: execute "what should I do in a day in tokyo?". This command will give you and output similar to:

You asked: UserInput(content=what should I do in a day in tokyo?, timestamp=2025-10-10T16:02:38.157335Z)

{
  "text" : "Start your day with a visit to the Meiji Shrine to experience traditional Japanese culture. Then, head to Harajuku for vibrant street fashion and lunch. In the afternoon, explore Shibuya, see the famous crossing, and visit Hachiko statue. Next, take the train to Asakusa to see Senso-ji Temple and shop along Nakamise Street. Finish your day in the evening at Tokyo Skytree for panoramic city views and dinner at Solamachi shopping complex."
}

but what if you want is a flight you can ask: execute "I want to flight to paris next week" and get something like:

You asked: UserInput(content=I want to flight to paris next week, timestamp=2025-10-10T16:03:11.831736Z)

{
  "text" : "Flight Number: AF007\nDeparture: JFK\nArrival: CDG\nDate: 2025-10-17T14:00"
}

Conclusion

Embabel provides a great alternative for building agents by embracing the strengths of strongly-typed languages like Java and Kotlin. It is a framework still under development but it already shows great potential.

So if you want to start experimenting with how you can integrate AI tools in your applications you can do that with Embabel and AI Agents now.

By the way, Embabel is built on the Spring Framework and Spring AI so everything you can use in Spring you can use within Embabel!

Find the documentation here: https://docs.embabel.com/embabel-agent/guide/0.1.3/