A Python implementation of an LLM-powered café order processing system using OpenAI function calling to handle natural language orders, inventory management, pricing, customer allergies, loyalty points, and customer communication.
- Natural Language Processing: Parse customer orders from free-text input
- Inventory Management: Real-time availability checking for all ingredients
- Allergy Protection: Automatic customer allergy checking with clear messaging
- Loyalty Points System: Redeem points in blocks of 100 for $5 off (minimum $5 subtotal)
- Dynamic Pricing: Size multipliers, modifier pricing, location-based tax calculation
- Smart Substitutions: Suggest alternatives when items are unavailable
- Customer Communication: Send messages for substitutions and clarifications
You are CafeOrderAssistant. Parse customer orders and MUST call appropriate functions. DO NOT provide explanations - only call functions and return final JSON.
Available menu items with BASE PRICES:
- DRIP: Drip Coffee (S/M/L) - BASE: $2.50 - modifiers: milk (none/whole/skim/oat/almond), shots (0-3)
- LATTE: Latte (S/M/L) - BASE: $4.00 - modifiers: milk (whole/skim/oat/almond), syrup (vanilla/caramel)
- MOCHA: Mocha (S/M/L) - BASE: $4.50 - modifiers: milk (whole/skim/oat/almond), choc (dark/milk)
- COOKIE: Cookie (no sizes) - BASE: $2.00 - allergens: gluten, nuts
PRICING RULES:
- Size multipliers: S ×1.0, M ×1.2, L ×1.4
- Modifiers: extra shot +$0.75 each; alt milks (oat/almond) +$0.50; syrups +$0.50
- Tax: 8.875% if location=NYC, else 0%
- ALWAYS use correct base prices in pricing_quote calls
STRICT WORKFLOW:
1. Parse order into normalized items with ALL modifiers
2. ALWAYS call menu_and_inventory_lookup with customer_id and ALL modifiers
3. If missing information (size, milk type), return ask_for_info action
4. If inventory issues or allergies, call messaging_send then return substitute_and_confirm
5. If everything OK, call pricing_quote then return fulfill action
6. Return ONLY final JSON decision - no explanations or markdown
CRITICAL RULES:
- For "latte" without size/milk → MUST ask_for_info
- For incomplete orders → MUST ask_for_info (combine missing fields)
- ALWAYS call menu_and_inventory_lookup with ALL modifiers before any decision
- ALWAYS include customer_id parameter for allergy checking
- If substitutions needed → call messaging_send first
- Default size: M if not specified (except when asking for info)
- ALWAYS use the exact location provided in the order context for pricing_quote
- For loyalty points: redeem in blocks of 100 for $5 off, cannot exceed subtotal
- When customer asks to "pay with points", suggest appropriate redemption amount
MESSAGE CLARITY RULES:
- If "cookie_gluten" in missing: say "cannot serve cookies due to your gluten allergy"
- If "cookie" in missing: say "out of cookies"
- If "almond_milk" in missing: say "out of almond milk"
- Be specific about WHY items cannot be served (allergy vs inventory)
FUNCTION CALL REQUIREMENTS:
- Every order MUST call menu_and_inventory_lookup with customer_id and complete modifiers first
- Include ALL modifiers in menu_and_inventory_lookup calls
- Use pricing_quote for fulfill actions - MUST include ALL modifiers in pricing_quote calls
- Use messaging_send for substitutions
- Return final decision as clean JSON only
FINAL RESPONSE FORMATS:
ask_for_info: {"action": "ask_for_info", "ask": {"missing": ["size", "milk"], "message": "..."}}
fulfill: {"action": "fulfill", "items": [...], "pricing": {...}}
substitute_and_confirm: {"action": "substitute_and_confirm", "substitutions": {...}, "message": "..."}
reject: {"action": "reject", "reason": "..."}
DO NOT explain your reasoning or provide markdown. Call functions then return JSON.
USER: "large mocha, dark chocolate, oat milk, 2 extra shots, and a cookie"
ASSISTANT → tool call: menu_and_inventory_lookup([...])
TOOL → {ok:true}
ASSISTANT → tool call: pricing_quote([...])
TOOL → {...}
ASSISTANT (final):
{
"order_id":"O1",
"action":"fulfill",
"items":[...],
"pricing":{...},
"notes":["LLM processed order successfully"]
}
USER: "latte"
ASSISTANT → tool call: menu_and_inventory_lookup([{"sku":"LATTE","size":"M","qty":1}], "CUST-002")
TOOL → {ok:false, missing:["milk_type"]}
ASSISTANT (final):
{
"action":"ask_for_info",
"ask": {"missing":["milk"],["size"], "message":"Please specify size and milk type"}
}
USER: "small latte with almond milk and a cookie" (Customer has gluten allergy)
ASSISTANT → tool call: menu_and_inventory_lookup([...], "CUST-002")
TOOL → {ok:false, missing:["almond_milk","cookie_gluten"], suggestions:{...}}
ASSISTANT → tool call: messaging_send("CUST-002", "We are out of almond milk and cannot serve cookies due to your gluten allergy. Would you like to substitute almond milk with oat milk and remove the cookie from your order?")
ASSISTANT (final):
{
"action":"substitute_and_confirm",
"substitutions":{"almond_milk":"oat_milk","cookie_gluten":"remove_cookie"},
"message":"We are out of almond milk and cannot serve cookies due to your gluten allergy..."
}
The system uses OpenAI-style function calling with these tool definitions:
TOOL_FUNCTIONS = [
{
"name": "menu_and_inventory_lookup",
"description": "Check menu items and inventory availability",
"parameters": {
"type": "object",
"properties": {
"items": {
"type": "array",
"description": "List of normalized items to check",
"items": {
"type": "object",
"properties": {
"sku": {"type": "string"},
"size": {"type": "string"},
"qty": {"type": "integer"},
"modifiers": {"type": "object"}
},
"required": ["sku"]
}
},
"customer_id": {
"type": "string",
"description": "Customer ID for allergy checking"
}
},
"required": ["items"]
}
},
{
"name": "pricing_quote",
"description": "Calculate order pricing with taxes and discounts",
"parameters": {
"type": "object",
"properties": {
"order": {
"type": "array",
"description": "List of items for pricing",
"items": {
"type": "object",
"properties": {
"sku": {"type": "string"},
"base_price": {"type": "number"},
"size": {"type": "string"},
"qty": {"type": "integer"},
"modifiers": {"type": "object"}
},
"required": ["sku", "base_price"]
}
},
"opts": {
"type": "object",
"properties": {
"location": {"type": "string"},
"loyalty_points_to_redeem": {"type": "integer"},
"customer_id": {"type": "string"}
},
"required": ["location", "customer_id"]
}
},
"required": ["order", "opts"]
}
},
{
"name": "messaging_send",
"description": "Send a message to customer",
"parameters": {
"type": "object",
"properties": {
"to": {"type": "string", "description": "Customer ID"},
"text": {"type": "string", "description": "Message text"}
},
"required": ["to", "text"]
}
}
]- Parse Order: LLM extracts items, sizes, modifiers from natural language
- Tool Selection: LLM decides which tools to call based on parsed information
- Function Execution: System executes tool calls and returns results to LLM
- Decision Making: LLM uses tool results to make final action decision
- Output Generation: LLM formats decision as required JSON structure
cafe_order_assistant/
├── data/
│ ├── menu.json # Menu items (DRIP, LATTE, MOCHA, COOKIE)
│ ├── inventory.json # Inventory levels (oat_milk, almond_milk, etc.)
│ ├── customers.json # Customer data (CUST-001, CUST-002)
│ └── orders.jsonl # Test orders (O1-O5)
├── src/
│ ├── agent.py # LLM agent with function calling
│ ├── tools.py # Tool implementations
│ ├── pricing.py # Exact pricing rules
│ └── logger.py # Logging and replay support
├── evaluate.py # Evaluation script
├── README.md # This file
├── replay.json # Generated by evaluate.py
└── outbox.log # Generated by messaging.send
- Input: List of normalized items with sku, size, modifiers, qty + customer_id for allergy checking
- Output:
{ok, missing, suggestions}with availability status and allergy conflicts - Checks inventory for components (oat_milk, almond_milk, cookie, etc.)
- Checks customer allergies and returns specific missing codes (e.g., "cookie_gluten" vs "cookie")
- Provides clear suggestions for substitutions
- Input: Order items and options (location, loyalty_points_to_redeem, customer_id)
- Output:
{subtotal, discounts, tax, total, redeemed_points} - Applies exact pricing rules:
- Size multipliers: S ×1.0, M ×1.2, L ×1.4 (cookies don't get size multipliers)
- Modifiers: extra shot +$0.75; alt milks (oat/almond) +$0.50; syrups +$0.50
- Tax: 8.875% if location=NYC, else 0%
- Loyalty: 100 points = $5 off (requires subtotal ≥ $5 for redemption)
- Updates customer loyalty points in customers.json when points are redeemed
- Input: Customer ID and message text
- Output:
{"status": "ok"} - Appends timestamped message to outbox.log
{
"id": "O1",
"customer_id": "CUST-001",
"text": "large mocha, dark chocolate, oat milk, 2 extra shots, and a cookie",
"meta": {"location": "NYC"}
}{
"order_id": "O1",
"action": "fulfill",
"items": [...],
"pricing": {...},
"notes": ["LLM processed order successfully"]
}| Order | Expected Action | Rationale | Key Features Tested |
|---|---|---|---|
| O1 | fulfill |
Large mocha + cookie with loyalty points | Loyalty redemption (100 pts = $5 off), size multipliers, modifier pricing |
| O2 | ask_for_info |
Latte missing milk type specification | Information gathering, clear messaging |
| O3 | substitute_and_confirm |
Almond milk OOS + cookie allergy protection | Clear messaging: "out of almond milk" vs "cannot serve cookies due to gluten allergy" |
| O4 | fulfill |
Drip coffee, no point redemption | Subtotal < $5 prevents loyalty redemption |
| O5 | substitute_and_confirm |
Milk chocolate OOS → dark chocolate | Inventory management with alternatives |
- Install dependencies:
pip install -r requirements.txt
### Dependencies
- `openai>=1.0.0` - OpenAI API client
- `python-dotenv>=1.0.0` - Environment variable management- Create
.envfile with your OpenAI API key:
OPENAI_API_KEY=your_actual_openai_api_key_here
OPENAI_MODEL=gpt-4
OPENAI_MAX_TOKENS=5000
OPENAI_TEMPERATURE=0.1cd cafe-order-assistant-authentic
python evaluate.py --livecd cafe-order-assistant-authentic
python evaluate.py --replay replay.jsonEvaluation Output:
- Tool call traces for each order showing function calls and results
- Final decisions with action types (fulfill/ask_for_info/substitute_and_confirm)
- Success/failure status against expected actions
- Message logs saved to
outbox.log - Complete replay data saved to
replay.jsonfor future testing