Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions python/agents/food-order-assistant/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Project Configuration.
export GOOGLE_CLOUD_PROJECT=<project id>
export GOOGLE_CLOUD_LOCATION=us-central1
export GOOGLE_GENAI_USE_VERTEXAI=1
# For Agent deployment.
export GOOGLE_CLOUD_STORAGE_BUCKET=<storage bucket used for agent deployment>
export LOG_LEVEL=INFO
export PYTHONPATH=$(pwd)/venv/bin/python:$(pwd)

82 changes: 82 additions & 0 deletions python/agents/food-order-assistant/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Customer Order Assistant

## Overview

This repository contains the implementation of a conversational AI assistant designed to streamline the customer ordering food items. The assistant can understand and process customer orders in natural language, making it suitable for various voice- and text-based channels.

Potential applications for this assistant include:
* **Food Ordering At Kiosk**: Providing a conversational interface for customers to place orders directly at a self-service kiosk.
* **Automated Drive-Through Ordering**: Integrating with drive-through systems to take orders, reducing wait times and improving order accuracy.
* **Phone-Based Ordering**: Handling inbound calls to take food orders, freeing up staff and ensuring a consistent customer experience.

**_NOTE:_** While this implementation focuses on food ordering, it can be easily generalized to take orders for other items.

## Technical Design

### Modular

The agent system is designed in a modular fashion, breaking down the food ordering process into a series of specialized sub-agents. Each agent is responsible for a distinct part of the conversation:

* The `preorder_agent` initiates the interaction and gathers preliminary details.
* The `menu_agent` focuses on taking the customer's food and drink orders.
* The `confirmation_agent` summarizes the order to ensure accuracy and confirms with the customer.
* The `user_info_agent` collects necessary customer details to finalize the order.

This modular design creates a clear and manageable workflow with several key benefits:
* **Maintainability**: Developers can work on, test, and improve each conversational component independently, leading to a more robust system.
* **Flexibility**: New use cases can be easily adopted in the future by composing or replacing existing sub-agents.

### Custom Workflow Agent

To create a predictable, low-latency conversational flow, this system uses a `CustomWorkflowAgent` to orchestrate the sub-agents. Instead of using an LLM to decide which agent to use next, it employs a deterministic, rule-based approach.

As seen in `agents/order_flow_agent.py`, a `get_next_agent` function inspects the `session_state` (e.g., `ORDER_TYPE`, `ORDER_STATUS`) to decide which specialized agent should handle the next turn. This state-driven logic ensures efficient and reliable transitions between sub-agents, decoupling the agent decision logic from the specific sub-agent implementation.


#### Agent Workflow Diagram
![Agent Workflow](assets/agents.png)

### Structured Outputs

To keep response latency low, the agents are designed to return structured JSON outputs consisting of the agent response and other fields indicating the state of the order processing. This allows the system to perform multiple operations—like updating session state and preparing the next user response—in a single, parallel step without needing additional LLM calls.

For example, a sub-agent, such as the `preorder_agent` in `agents/preorder_agent/agent.py` and the `menu_agent` in `agents/menu_agent/menu_agent.py`, defines an `OutputSchema` using Pydantic. For instance, the `menu_agent`'s schema includes fields for `agent_response`, `order_update`, and `order_finished`.

An `AfterModelCallback` function in each agent parses the LLM's JSON output. It then uses this structured data to deterministically update the session state (e.g., `ORDER_STATUS`) and formulate the exact text to return to the user. This approach avoids unpredictable text parsing and minimizes LLM calls, ensuring a faster and reliable user experience.

## Environment Setup

```
$ cp .env.example .env

# Provide the specific values for the environment variables
$ nano .env

# Set the environment variables
$ source .env
```

## Run Locally

```
# Set up venv
$ python3 -m venv venv
$ source venv/bin/activate

# Install dependencies
$ pip install -r requirements.txt

# Run adk web
$ adk web --trace_to_cloud .
```

## Deploy in Agent Engine

```
$ python3 deployment/deploy.py
```

## Example Conversation
![Agent Workflow](assets/convo-1.png)
![Agent Workflow](assets/convo-2.png)
![Agent Workflow](assets/convo-3.png)
1 change: 1 addition & 0 deletions python/agents/food-order-assistant/agent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import agent
38 changes: 38 additions & 0 deletions python/agents/food-order-assistant/agent/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import logging
import os

from google import genai
from google.genai.types import (
HttpOptions,
)

from . import order_flow_agent

# This code is for the ADK web dev UI. (https://github.com/google/adk-web)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

genai_client = genai.Client(
vertexai=True,
project=os.environ["GOOGLE_CLOUD_PROJECT"],
location=os.environ["GOOGLE_CLOUD_LOCATION"],
http_options=HttpOptions(api_version="v1"))

root_agent = order_flow_agent.create_agent(
genai_client=genai_client)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import confirmation_agent
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from typing import Union

from google.adk.agents.callback_context import CallbackContext
from google.adk.agents import LlmAgent
from google.adk.models.base_llm import BaseLlm
from google.adk.models.llm_response import LlmResponse
from google.adk.planners.built_in_planner import BuiltInPlanner
from google.genai import types
from pydantic import BaseModel, ConfigDict, Field

from .. import constants

import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class OutputSchema(BaseModel):
"""Output schema for the agent."""
agent_response: str = Field(description="Assistant response to the user")
order_confirmed: str = Field(description="true, if the user has confirmed, false, if the user wants to change the order. unknown if the user didn't respond yet.")


class AfterModelCallback:
def __call__(
self,
callback_context: CallbackContext,
llm_response: LlmResponse,
):
output = OutputSchema.model_validate_json(llm_response.content.parts[0].text)
logger.info(f"Model output: {output}")

state = callback_context.state
if output.order_confirmed != "unknown":
if output.order_confirmed == "true":
state[constants.ORDER_STATUS] = constants.CONFIRMED
else:
state[constants.ORDER_STATUS] = constants.IN_PROGRESS

llm_response.content = types.Content(
role='model',
parts=[types.Part(text=output.agent_response)],
)
return llm_response


class ConfirmationAgent(LlmAgent):

def __init__(self, model: Union[str, BaseLlm], **kwargs):
super().__init__(
model=model,
name='confirmation_agent',
planner=BuiltInPlanner(
thinking_config=types.ThinkingConfig(
thinking_budget=0,
include_thoughts=False
)
),
instruction="""
You are confirmation_agent to help the user confirm the order.

You provide the brief summary of the items in the cart and ask if the order is correct.
If the user confirms the order, return an empty text for agent_response and set order_confirmed to true.
If the user wants to change the order or asks for correction, set order_confirmed to false.

Current cart: {current_order?}
Order total: ${order_total?}

<example>
Cart: < a) 1 dave's #1: 2 tenders with fries>
Output: {"agent_response": "Alright, <summary of items in cart and the total cost>. Would that be all for today?",
"order_confirmed": "unknown"}
</example>
<example>
Cart: < a) 1 dave's #1: 2 tenders with fries>
Output: {"agent_response": "Alright, <summary of items in cart and the total cost>. Would that be all for today?",
"order_confirmed": "unknown"}
User: that's it
Output: {"agent_response": "", "order_confirmed": "true"}
</example>
<example>
Cart: < a) 1 dave's #1: 2 tenders with fries>
Output: {"agent_response": "Alright, <summary of items in cart and the total cost>. Would that be all for today?",
"order_confirmed": "unknown"}
User: no
Output: {"agent_response": "", "order_confirmed": "false"}
</example>
<example>
Cart: < a) 1 dave's #1: 2 tenders with fries>
Output: {"agent_response": "Alright, <summary of items in cart and the total cost>. Would that be all for today?",
"order_confirmed": "unknown"}
User: Say that again?
Output: {"agent_response": "You ordered <summary of items in cart and the total cost>. Would that be all for today?",
"order_confirmed": "unknown"}
</example>
""",
output_schema=OutputSchema,
disallow_transfer_to_parent=True,
disallow_transfer_to_peers=True,
after_model_callback=AfterModelCallback(),
)


def create_agent(llm: Union[str, BaseLlm]):
return ConfirmationAgent(model=llm)
33 changes: 33 additions & 0 deletions python/agents/food-order-assistant/agent/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

ORDER_TYPE="order_type"
ORDER_STATUS="order_status"
CURRENT_ORDER="current_order"
ORDER_TOTAL="order_total"
NAME_FOR_ORDER="name_for_order"

# Order status
CONFIRMED="confirmed"
FINISHED="finished"
IN_PROGRESS="in_progress"
GETTING_USER_INFO="getting_user_info"

# Agent names
PREORDER_AGENT="preorder_agent"
MENU_AGENT="menu_agent"
CONFIRMATION_AGENT="confirmation_agent"
USER_INFO_AGENT="user_info_agent"
DONE="done"

90 changes: 90 additions & 0 deletions python/agents/food-order-assistant/agent/custom_workflow_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import logging

from google.adk.agents import LlmAgent, BaseAgent
from google.adk.agents.invocation_context import InvocationContext
from google.adk.events import Event

from typing import AsyncGenerator, Callable
from typing_extensions import override


logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

_MAX_ITERATIONS = 30 # To prevent infinite loop.
_DONE_AGENT = "done" # Special agent name when the workflow is done.

class CustomWorkflowAgent(BaseAgent):
"""
This agent orchestrates a list of sub LLM agents to run based on the session state.
"""

agent_map: dict[str, LlmAgent] = {}
get_next_agent: Callable
step: int = 0

model_config = {"arbitrary_types_allowed": True}

def __init__(
self,
name: str,
agent_map: dict[str, LlmAgent],
get_next_agent: Callable,
):
"""
Initializes the OrderFlowAgent.
"""
sub_agents_list = list(agent_map.values())

super().__init__(
name=name,
agent_map=agent_map,
get_next_agent=get_next_agent,
sub_agents=sub_agents_list,
)

@override
async def _run_async_impl(
self, ctx: InvocationContext
) -> AsyncGenerator[Event, None]:
"""
Implements the custom orchestration logic for the order workflow.
"""
session_state = ctx.session.state
selected_agent_name = self.get_next_agent(session_state)
while self.step < _MAX_ITERATIONS:
self.step += 1

logger.info(f"Step {self.step}: selected_agent -- {selected_agent_name}")
selected_agent = self.agent_map.get(selected_agent_name, None)
if selected_agent == _DONE_AGENT:
# We are done.
logger.info("Final state: {session_state}")
return

if not selected_agent:
logger.error(f"ERROR: Unknown agent name: {selected_agent_name}")
return

async for event in selected_agent.run_async(ctx):
yield event
next_agent = self.get_next_agent(session_state)
if next_agent == selected_agent_name:
# Exits to get another user input.
return
selected_agent_name = next_agent
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import menu_agent
Loading