AI Engineer In 10 Weeks | Week 4 Mini Project: Design Your Restaurant AI Agent
This chapter introduces the Week 4 mini-project: designing and implementing a stateful Restaurant AI Agent using LangGraph. Before diving into the technical specifications, we address a critical industry concern: Will AI replace software engineers? We will explore why coding was never the core value of software engineering, how the industry is shifting toward architectural thinking, and how to build a multi-tool agent capable of handling complex, stateful loops and failure recoveries.
1. The Reality of Software Engineering in the AI Era
A common anxiety among aspiring developers is that because AI models (like ChatGPT, Claude, and Gemini) can write code instantly, software engineering jobs will disappear. This fear stems from a fundamental misunderstanding of what a software engineer's job actually is.
Historically, writing code has never been the highest-valued part of software engineering. It is merely the translation layer between human thought and computer execution.
SDE-1 vs. Principal Engineer: The Value Split
To understand where the real value lies, consider the organizational hierarchy and compensation structure of a typical tech company:
If writing code was the core value of software engineering, freshers would be paid ₹50 LPA and Principal Engineers would be paid ₹10 LPA. In reality, as you get promoted, the amount of code you write decreases, while your compensation increases.
2. The Shift from Syntax Memorization to Architectural Thinking
The software development lifecycle consists of several phases. Writing code is actually the slowest, most tedious, and least intellectually demanding phase:
- The Old Way: Developers spent months looking up syntax, debugging typos, searching StackOverflow, and writing boilerplate code. Because this phase took the longest, it was handed off to the least experienced team members (freshers).
- The AI Way: AI has automated this translation layer. You describe what you want in natural language, and the AI generates the syntax in seconds.
Actionable Advice for AI Engineers
Stop stressing over memorizing syntax. If you forget how to add a node in LangGraph, how to register a tool, or how to format a JSON payload, it does not matter. AI can write that boilerplate for you instantly.
Instead, focus your energy on architectural thinking:
- Why should we use a specific data structure (e.g., a Heap vs. a Binary Search Tree)?
- How should state flow between different components of our system?
- Which database is appropriate for this scale, and why?
- How do we design robust fallback mechanisms when an LLM or tool fails?
3. Project Overview: The Restaurant AI Agent
For this mini-project, you will build a stateful Restaurant AI Agent using LangGraph. An AI Agent is defined as:
Your agent will act as an automated waiter and kitchen coordinator. It must interact with customers, check a live database/menu, handle inventory constraints, coordinate with the kitchen, manage delivery/serving, and gracefully recover from real-world failures.
Strict Scope Boundary (Guardrails)
Your agent is a specialized restaurant bot, not a general-purpose assistant.
- If a user asks about food, menus, or ordering, the agent must process it.
- If a user asks an out-of-scope question (e.g., "What is the capital of India?", "Write a Python program for Two Sum", or "Generate a 1980s photo"), the agent must politely refuse to answer.
4. The Shared State & Menu Data Structure
In LangGraph, nodes communicate by reading from and writing to a shared State (which acts as a shared whiteboard).
The Menu Database
Your agent will interact with a mock database representing the restaurant's menu. Each item has a price and an available quantity:
The Shared State Schema
Your LangGraph state must keep track of variables such as:
customer_query: The raw input from the user.selected_dish: The dish identified by the LLM.quantity: The number of plates requested.retry_count: An integer tracking how many times the order process has failed/retried (maximum of 3).agent_response: The message to be returned to the user.
5. Step-by-Step Tool Specifications & Failure Modes
Your agent must orchestrate three primary tools. To make the project realistic, each tool must include simulated failure modes.
Tool 1: take_order
- Inputs:
dish_name(str),quantity(int) - Logic:
- Check if the dish exists in the menu.
- If it exists, check if the requested quantity is available.
- Failure Mode A (Item Not Found): If the user orders something unavailable (e.g., "Elephant Sandwich"), the tool returns a failure. The LLM must catch this, print the available menu, and ask the user to choose another dish.
- Failure Mode B (Insufficient Quantity): If the user orders 20 plates of Paneer Butter Masala but only 10 are left, the tool returns a failure. The LLM must inform the user: "We only have 10 plates left. Would you like to confirm these 10 plates, or choose something else?"
- The 3-Retry Rule: The user is allowed a maximum of 3 attempts to correct their order. If they fail to place a valid order within 3 turns, the agent must cancel the interaction and end the program with a polite apology.
Tool 2: cook
- Logic: Simulates cooking the confirmed order.
- Failure Mode: Introduce an unexpected kitchen failure (e.g., "Kitchen ran out of gas" or "Ingredient spoiled").
- Recovery Flow: If cooking fails, the agent must report the issue to the customer, navigate back to the ordering stage, and ask them to select a different dish.
Tool 3: serve
- Logic: Simulates delivering the food to the customer's table.
- Failure Mode: Introduce a delivery mishap (e.g., "The waiter dropped the plate").
- Recovery Flow: If serving fails, the agent must automatically route the flow back to the
cooknode to remake the dish, rather than forcing the customer to re-order.
6. The State Machine Flowchart
Below is the architectural flow of the LangGraph agent. Note how the state loops backward depending on tool outputs and failures:
7. Why LangGraph? (Naive vs. Agentic State Machine)
If you tried to build this system using standard Python code, you would quickly end up with a highly nested, unmaintainable mess of if-else blocks and while loops:
# Naive, non-agentic approach (Hard to scale and maintain)
order = take_order()
if not order.success:
# Handle retry 1
order = retry_order()
if not order.success:
# Handle retry 2...
else:
cooked = cook(order)
if not cooked.success:
# Go back to order...
else:
served = serve(cooked)
if not served.success:
# Go back to cook...
This naive approach breaks down completely when you scale from 3 functions to 1,000 functions.
LangGraph solves this by allowing you to define a clean Directed Acyclic Graph (DAG) or stateful cyclic graph where:
- Each step is an isolated Node (a Python function).
- Transitions are governed by Edges and Conditional Edges (routing functions).
- The state is automatically preserved and passed between steps.
8. Architectural Code Skeleton (Python & LangGraph)
To help you start building, here is the conceptual code skeleton for your LangGraph agent. Use this structure to implement your solution before the next lecture.
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END
# 1. Define the Shared State
class AgentState(TypedDict):
customer_query: str
selected_dish: str
quantity: int
retry_count: int
status: str # "ordering", "cooking", "serving", "completed", "failed"
response_message: str
# Mock Menu Database
MENU = {
"paneer butter masala": {"price": 300, "quantity": 10},
"chicken burger": {"price": 450, "quantity": 20}
}
# 2. Define Node Functions
def llm_router_node(state: AgentState):
"""
Analyzes customer query. Checks if it is in-scope.
Extracts dish and quantity if valid.
"""
query = state["customer_query"].lower()
# Simple keyword check (Replace with actual LLM call in your project)
if "paneer" in query or "burger" in query:
# Extract details...
return {
"selected_dish": "paneer butter masala" if "paneer" in query else "chicken burger",
"quantity": 1, # Mock extracted quantity
"status": "ordering"
}
else:
return {
"status": "failed",
"response_message": "I can only help you with restaurant orders. How can I help you dine today?"
}
def take_order_node(state: AgentState):
dish = state["selected_dish"]
qty = state["quantity"]
retries = state.get("retry_count", 0)
if dish not in MENU:
return {
"retry_count": retries + 1,
"status": "invalid_order",
"response_message": f"Sorry, {dish} is not on our menu."
}
available_qty = MENU[dish]["quantity"]
if qty > available_qty:
return {
"retry_count": retries + 1,
"status": "invalid_order",
"response_message": f"We only have {available_qty} of {dish} left."
}
return {"status": "cooking", "response_message": "Order confirmed! Sending to kitchen."}
def cook_node(state: AgentState):
# Simulate cooking with a potential failure
import random
cooking_failed = random.choice([True, False]) # Simulate real-world unpredictability
if cooking_failed:
return {
"status": "cooking_failed",
"response_message": "Apologies, we ran into an issue in the kitchen. Let's change your order."
}
return {"status": "serving", "response_message": "Food is cooked! Waiter is serving it now."}
def serve_node(state: AgentState):
import random
serving_failed = random.choice([True, False]) # Simulate dropped plate
if serving_failed:
return {
"status": "serving_failed",
"response_message": "Oh no! The waiter dropped the plate. Remaking the dish immediately!"
}
return {"status": "completed", "response_message": "Enjoy your meal! Here is your bill."}
# 3. Define Routing Logic (Conditional Edges)
def route_after_order(state: AgentState) -> Literal["cook", "retry_or_cancel"]:
if state["status"] == "cooking":
return "cook"
return "retry_or_cancel"
def route_after_retry(state: AgentState) -> Literal["router", "end"]:
if state["retry_count"] >= 3:
return "end"
return "router"
def route_after_cooking(state: AgentState) -> Literal["serve", "router"]:
if state["status"] == "cooking_failed":
return "router" # Go back to the beginning to choose another dish
return "serve"
def route_after_serving(state: AgentState) -> Literal["cook", "end"]:
if state["status"] == "serving_failed":
return "cook" # Go back to cooking node, do not ask user to re-order
return "end"
# 4. Build the Graph
workflow = StateGraph(AgentState)
# Add Nodes
workflow.add_node("router", llm_router_node)
workflow.add_node("take_order", take_order_node)
workflow.add_node("cook", cook_node)
workflow.add_node("serve", serve_node)
# Set Entry Point
workflow.set_entry_point("router")
# Add Normal Edges
workflow.add_edge("router", "take_order")
# Add Conditional Edges
workflow.add_conditional_edges(
"take_order",
route_after_order,
{
"cook": "cook",
"retry_or_cancel": "router" # Simplified routing back to user input
}
)
workflow.add_conditional_edges(
"cook",
route_after_cooking,
{
"serve": "serve",
"router": "router"
}
)
workflow.add_conditional_edges(
"serve",
route_after_serving,
{
"cook": "cook",
"end": END
}
)
# Compile Graph
app = workflow.compile()
9. Summary and Key Takeaways
- Coding is Not the Core Value: Writing code is the translation of human thoughts into computer-understandable syntax. AI has automated this step. The true value of a software engineer lies in problem-solving, architecture, and system design.
- Do Not Memorize Syntax: Focus on understanding concepts, workflows, and system design. Use AI tools (like Claude or ChatGPT) to handle the syntax generation.
- Agentic Workflows: Agents combine LLMs with tools to perform complex, multi-step tasks.
- State Management: LangGraph uses a centralized "State" object that acts as a shared whiteboard for all nodes in the graph.
- Cyclic Graphs: Unlike traditional linear pipelines, LangGraph allows cycles (loops), which are essential for handling retries and error recovery.
- Failure Recovery Design: Robust agents must handle real-world failures gracefully. For example, if cooking fails, the agent routes back to ordering; if serving fails, it routes back to cooking without bothering the customer.
- Strict Scope Guardrails: Specialized agents must reject out-of-scope queries (like general knowledge or coding questions) to maintain system integrity.