✈️ Travel Guide#
✈️ Travel Guide sends one chat request to OpenAI (gpt-4o-mini) with a system persona and a user prompt, and returns the model's single travel suggestion for the city you type.
How It Works#
The idea behind the demo, the request it sends and the function calls that answer it.
Concept#
Travel Guide is the foundational building block of every LLM application: a
stateless, single-turn call to the OpenAI Chat Completions API using
gpt-4o-mini. Given any city name (such as "Bangalore" or "Kyoto"),
the service constructs a two-message conversation and returns a concise, witty travel
recommendation.
Even in this minimal example, the code separates who the model should be from what the user is asking:
- System Message (Persona & Guardrails): Sets the behavioral frame
(
"You are a witty travel guide.") so the tone remains consistent regardless of which city the user types. - User Message (Task Input): Injects the dynamic variable
(
f"Suggest one thing to do in {city}.") into a clear, scoped instruction. - Production Client Hygiene: Uses environment-driven configuration
(
OPENAI_API_KEYandOPENAI_MODEL) and enforces a strict 15-second network timeout so a stalled upstream call never hangs the Flask worker.
Theory#
1. The Chat Completions Message Protocol
Modern instruction-tuned LLMs do not just complete raw text strings; they consume an ordered
array of role-tagged messages (messages=[...]). Each message has a
role and content:
system— high-priority instructions that establish persona, tone, constraints, and output expectations before the user speaks.user— the prompt, question, or task payload supplied by the human or calling application.assistant— responses generated by the model (used in multi-turn conversations and tool loops).
2. Stateless Inference by Design
Every call to client.chat.completions.create() is completely independent. The
OpenAI API retains no memory of previous requests. If you ask for "Bangalore" and then
immediately ask "What about food there?", a stateless endpoint has no idea what
"there" refers to unless earlier messages are re-sent (which the Memory
Chat demo addresses).
3. Navigating the Response Envelope
The API returns a structured ChatCompletion object containing token usage metrics,
model metadata, finish reasons, and a list of candidate choices. Because we request
a single completion (n=1 by default), the generated text is extracted from
response.choices[0].message.content, with or "" guarding against
None when a response contains only tool calls or refusal metadata.
4. Externalized Configuration & Timeouts
Hardcoding API keys or model IDs inside application code makes key rotation and model upgrades
risky. Loading OPENAI_API_KEY and OPENAI_MODEL via
python-dotenv keeps secrets out of version control, while
OpenAI(timeout=15.0) protects server threads from indefinite socket hangs.
Request flow#
index.html + main.js sends POST /api/travel with { city }.
app.py)
travel_suggestion() checks the city is not empty and calls get_travel_suggestion(city).
01_chat_openai.py)
Builds the system + user messages and calls client.chat.completions.create().
{ city, suggestion }.
Code explanation#
Core AI Module (src/python/01_chat_openai.py)
src/python/01_chat_openai.py implements a single-turn chat completion call using the official OpenAI Python SDK. It defines typed Pydantic data contracts and formats a role-structured conversation (system persona + user task prompt) to generate a concise travel recommendation for any city.
Pydantic Schemas and AI Service Function
class TravelRequest(BaseModel): Defines the input schema withcity: str = "Bangalore"as the target destination for the travel query.class TravelResponse(BaseModel): Defines the output schema pairing the inputcity: strwith the model-generatedsuggestion: str.get_travel_suggestion(city: str = "Bangalore") -> str: Generates a witty travel suggestion from OpenAI through five numbered steps:- Step ① — Initialize the OpenAI Client: Instantiates
client = OpenAI(timeout=15.0), which authenticates withOPENAI_API_KEYand sets a 15-second request timeout. - Step ② — Select the Target Model: Reads the model identifier via
model = os.getenv("OPENAI_MODEL", "gpt-4o-mini"), defaulting togpt-4o-minifor fast, cost-efficient single-turn generation. - Step ③ — Build Role-Tagged Prompt Messages: Constructs the two-turn
messageslist separating behavioral instructions from the user's task:{"role": "system", "content": "You are a witty travel guide."}sets the assistant's persona and tone.{"role": "user", "content": f"Suggest one thing to do in {city}."}injects the requested city into a focused task prompt.
- Step ④ — Execute Chat Completion Inference: Calls
response = client.chat.completions.create(model=model, messages=messages)to send the role-structured prompt to OpenAI's Chat Completions API. - Step ⑤ — Extract the Assistant's Reply: Unwraps the first completion candidate via
response.choices[0].message.content or ""and returns the plain text recommendation.
- Step ① — Initialize the OpenAI Client: Instantiates
How the AI Components Connect End-to-End
- A target
citystring (modeled byTravelRequest) is passed intoget_travel_suggestion(city). - The function combines the persistent
systempersona ("You are a witty travel guide.") with the dynamicuserprompt (f"Suggest one thing to do in {city}.") into a singlemessagesarray. client.chat.completions.create()sends the message array and model name (gpt-4o-mini) to OpenAI, where the model conditions its response on both the persona and the city.- The generated text is extracted from
response.choices[0].message.contentto fulfill theTravelResponsecontract (cityandsuggestion).
Code flow#
POST /api/travel"]:::entry -->|"city"| R["app.py
travel_suggestion()"]:::fn R -->|"city"| S["01_chat_openai.py
get_travel_suggestion()"]:::fn S -->|"system persona + user prompt"| O[("OpenAI API
gpt-4o-mini")]:::ext O -->|"suggestion text"| S S -->|"suggestion text"| R R -->|"JSON { city, suggestion }"| UI classDef entry fill:#1e3a5f,stroke:#90caf9,color:#e0e0e0 classDef fn fill:#1e1e1e,stroke:#4caf50,color:#e0e0e0 classDef ext fill:#2d1f3d,stroke:#ce93d8,color:#e0e0e0 classDef ret fill:#1b3320,stroke:#81c784,color:#e0e0e0
Source Code#
Every Python file this demo runs, complete and unedited: the service implementation first, then the Flask Blueprint route handler (app.py) that exposes it.
01_chat_openai.py#
OpenAI Chat Completion Service - Travel Guide.
"""OpenAI Chat Completion Service - Travel Guide.
This service demonstrates direct LLM chat completion using the official OpenAI API:
1. Loads API credentials and model configuration from environment variables (.env).
2. Uses Pydantic models (TravelRequest, TravelResponse) for type validation and HTTP serialization.
3. Formats system and user prompts to guide the LLM's persona and task.
4. Executes the OpenAI Chat Completion endpoint with a 15-second safety timeout.
"""
import os
from dotenv import find_dotenv, load_dotenv
from openai import OpenAI
from pydantic import BaseModel
# ---------------------------------------------------------------------------
# Configuration & Environment Setup
# ---------------------------------------------------------------------------
# Automatically locate and load the nearest .env file (e.g. at the repo or project root)
load_dotenv(find_dotenv())
# ---------------------------------------------------------------------------
# Request & Response Schemas (Pydantic)
# ---------------------------------------------------------------------------
class TravelRequest(BaseModel):
"""Input payload for travel suggestion queries."""
city: str = "Bangalore"
class TravelResponse(BaseModel):
"""Output payload containing the target city and generated suggestion."""
city: str
suggestion: str
# ---------------------------------------------------------------------------
# Core Service Logic
# ---------------------------------------------------------------------------
def get_travel_suggestion(city: str = "Bangalore") -> str:
"""Suggest one thing to do in a given city using OpenAI.
Args:
city: Target city name (defaults to "Bangalore").
Returns:
A witty travel recommendation generated by the OpenAI model.
"""
# ① Initialize the OpenAI client (reads OPENAI_API_KEY from environment)
# with a 15-second safety timeout
client = OpenAI(timeout=15.0)
# ② Fetch model identifier configured in .env (defaults to gpt-4o-mini if unspecified)
model = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
# ③ Build the conversation messages:
# - system message: assigns the persona/tone ("witty travel guide")
# - user message: provides the specific prompt instruction
messages = [
{"role": "system", "content": "You are a witty travel guide."},
{"role": "user", "content": f"Suggest one thing to do in {city}."},
]
# ④ Call the Chat Completion endpoint
response = client.chat.completions.create(model=model, messages=messages)
# ⑤ Extract and return the assistant's reply
return response.choices[0].message.content or ""
app.py#
Flask Blueprint server and route handler (travel_suggestion) in src/python/app.py.
"""Flask server for 01-ai-basics."""
import importlib.util
import os
import sys
from pathlib import Path
from dotenv import find_dotenv, load_dotenv
from flask import Blueprint, Flask, jsonify, request
from flask_cors import CORS
from rate_limiter import check_rate_limit
load_dotenv(find_dotenv())
PATH_PREFIX = os.environ.get("PATH_PREFIX", "")
STATIC_DIR = Path(__file__).resolve().parents[1]
app = Flask(__name__, static_folder=str(STATIC_DIR))
CORS(app)
bp = Blueprint("main", __name__)
_CUR_DIR = Path(__file__).resolve().parent
def _load_service(filename: str, alias: str):
# ① Resolve the numbered service file path and create its import spec
file_path = _CUR_DIR / filename
spec = importlib.util.spec_from_file_location(f"services.{alias}", file_path)
if spec is None or spec.loader is None:
raise ImportError(f"Cannot load module from {file_path}")
# ② Register and execute the module so its functions can be called by routes
mod = importlib.util.module_from_spec(spec)
sys.modules[f"services.{alias}"] = mod
spec.loader.exec_module(mod)
return mod
mod_travel = _load_service("01_chat_openai.py", "chat_openai")
mod_joke = _load_service("02_chat_groq.py", "chat_groq")
@bp.before_request
def enforce_rate_limit():
# ① Only rate-limit mutating POST requests
if request.method == "POST":
# ② Check whether this client has exceeded 30 requests per hour
blocked, msg, retry_after = check_rate_limit(
request, max_requests=30, window_seconds=3600
)
if blocked:
# ③ Return HTTP 429 with Retry-After header when rate-limited
resp = jsonify({"error": msg})
resp.status_code = 429
resp.headers["Retry-After"] = str(retry_after)
return resp
@bp.route("/")
def index():
# ① Read index.html and inject the runtime PATH_PREFIX into data-api-base
with open(os.path.join(app.static_folder, "index.html"), encoding="utf-8") as f:
html = f.read()
html = html.replace('data-api-base=""', f'data-api-base="{PATH_PREFIX}"')
# ② Return the patched HTML page
return app.response_class(html, mimetype="text/html")
@bp.route("/css/<path:filename>")
def css(filename):
return app.send_static_file(os.path.join("css", filename))
@bp.route("/js/<path:filename>")
def js(filename):
return app.send_static_file(os.path.join("js", filename))
@bp.route("/explanation/<path:filename>")
@bp.route("/info/<path:filename>")
def explanation_pages(filename):
return app.send_static_file(os.path.join("explanation", filename))
@bp.route("/theory/<path:filename>")
def theory_pages(filename):
"""Serve the theory documentation pages from src/theory."""
return app.send_static_file(os.path.join("theory", filename))
@bp.route("/api/travel", methods=["POST"])
def travel_suggestion():
# ① Parse city from JSON payload and reject empty values
data = request.get_json(force=True, silent=True) or {}
city = (data.get("city") or data.get("message") or "").strip()
if not city:
return jsonify({"error": "City cannot be empty"}), 400
try:
# ② Call get_travel_suggestion() in 01_chat_openai.py and return JSON
suggestion = mod_travel.get_travel_suggestion(city)
return jsonify({"result": suggestion, "city": city, "suggestion": suggestion})
except Exception as exc:
return jsonify({"error": str(exc)}), 500
@bp.route("/api/joke", methods=["POST"])
def joke_generator():
# ① Parse topic from JSON payload and reject empty values
data = request.get_json(force=True, silent=True) or {}
topic = (data.get("topic") or data.get("message") or "").strip()
if not topic:
return jsonify({"error": "Topic cannot be empty"}), 400
try:
# ② Call get_groq_joke() in 02_chat_groq.py and return JSON
joke = mod_joke.get_groq_joke(topic)
return jsonify({"result": joke, "topic": topic, "joke": joke})
except Exception as exc:
return jsonify({"error": str(exc)}), 500
app.register_blueprint(bp, url_prefix=PATH_PREFIX)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Quick Revision#
Key AI concepts, architectural patterns, and practical takeaways from this demo.
Quick revision#
A numbered reference of the foundational AI and prompt-engineering concepts used in Travel Guide:
- Chat Completions Message Array: Modern LLMs consume an ordered list of role-tagged objects (
{"role": ..., "content": ...}) rather than a single flat text string, separating instructions from user input. - System Role Persona Framing: The
systemmessage ("You are a helpful travel guide.") conditions the model's tone, domain focus, and vocabulary before it reads the user's request. - Inline Output Length & Cardinality Constraints: Specifying "one must-visit place" and "in 2 sentences" directly inside the user prompt bounds response verbosity and latency without hard-truncating tokens via
max_tokens. - Cost-Efficient Small Frontier Models (
gpt-4o-mini): Usinggpt-4o-minifor focused single-turn generation provides low latency and a fraction of the token cost of larger reasoning models while maintaining high fluency. - Response Object Navigation (
choices[0].message.content): The OpenAI SDK returns aChatCompletionenvelope containing a list of candidatechoices, tokenusagemetadata, and the assistant's generatedmessage.content. - Defensive Timeout Configuration (
timeout=15.0): Setting an explicit 15-second timeout on theOpenAIclient prevents hung upstream connections from blocking web worker threads. - Decoupled Environment Credentials (
dotenv): LoadingOPENAI_API_KEYandOPENAI_MODELfrom.envviapython-dotenvkeeps secrets outside source code and allows model upgrades via configuration alone. - Boundary Validation & Error Normalization: Rejecting blank cities with HTTP
400and catching upstream SDK exceptions as HTTP500JSON ensures the frontend always receives a predictable JSON contract.