Role control
Ask one question, then switch the role behind it — plain teacher, skeptical
Showcase — Role control
Ask one question, then switch the role behind it — plain teacher, skeptical engineer, explain-to-a-nine-year-old, bulleted list. The question stays the same. Only the system prompt changes, and the answer changes with it.
Run
bash bootstrap-secrets.sh # reads ../../../../.env, writes secrets/
docker compose up --build # default: PROVIDER=openai
Open http://localhost:3000.
To run against Gemini instead:
PROVIDER=gemini docker compose up --build
What's where
backend/ai_openai.py— theROLESmap (the lesson) + the OpenAI callbackend/ai_gemini.py— sameROLESmap, Gemini callbackend/main.py— identical FastAPI loader; reads PROVIDER and dispatchesfrontend/app/page.tsx— textarea + role<select>+ resultdocker-compose.yml— two services, secrets mounted from./secrets/
The frontend encodes <role>\n---\n<question> into the single-string input
the backend expects, which keeps the def run(input: str) -> str contract
identical across every showcase in this week.
Stop
docker compose down
Run locally
Download the project as a ZIP and run it with Docker. Brings up a FastAPI backend + Next.js frontend on localhost:3000.
unzip role-control.zip
cd role-control
bash bootstrap-secrets.sh # one-time: pulls API keys into ./secrets
docker compose up --build # default provider: openai
# or: PROVIDER=gemini docker compose up --build
Type some input, pick a provider, and run the same code shown in Source against the live API. Sign-in required.
The same modules the Run button hits. The whole project (frontend, Dockerfile, compose) is in the ZIP under README.
backend/ai_openai.py
"""Week 2 - Showcase 1 (OpenAI): same question, different system prompt.
The whole lesson lives in ROLES. The user's text never changes; the
system instruction does, and the answer changes with it. The frontend
encodes the input as "<role>\\n---\\n<text>" so the
def run(input: str) -> str contract stays identical to every other showcase.
"""
from openai import OpenAI
_client = OpenAI()
# Each role is a system prompt. This is the only thing that varies between
# the answers — the user's question is passed through untouched.
ROLES = {
"plain": "You are a patient teacher. Explain in plain language a beginner "
"can follow. At most four sentences. No jargon without a gloss.",
"skeptic": "You are a skeptical staff engineer in a design review. Push "
"back. Name the risk or hidden cost first, then concede what "
"actually holds up. Three sentences, blunt.",
"five": "You are explaining to a curious nine-year-old. Use one everyday "
"analogy. Two short sentences. No technical terms at all.",
"bullets": "You answer only as a tight bulleted list. Three to five "
"bullets, each under twelve words. No intro line, no summary.",
}
def run(payload: str) -> str:
role, _, text = payload.partition("\n---\n")
system = ROLES.get((role or "plain").strip(), ROLES["plain"])
text = (text or payload).strip()
response = _client.responses.create(
model="gpt-5.4-nano",
instructions=system,
input=text,
max_output_tokens=600,
)
return response.output_text
backend/ai_gemini.py
"""Week 2 - Showcase 1 (Gemini): same question, different system prompt."""
import os
from google import genai
from google.genai import types
_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
# Identical role set to the OpenAI module, so swapping PROVIDER changes the
# SDK underneath without changing the lesson.
ROLES = {
"plain": "You are a patient teacher. Explain in plain language a beginner "
"can follow. At most four sentences. No jargon without a gloss.",
"skeptic": "You are a skeptical staff engineer in a design review. Push "
"back. Name the risk or hidden cost first, then concede what "
"actually holds up. Three sentences, blunt.",
"five": "You are explaining to a curious nine-year-old. Use one everyday "
"analogy. Two short sentences. No technical terms at all.",
"bullets": "You answer only as a tight bulleted list. Three to five "
"bullets, each under twelve words. No intro line, no summary.",
}
def run(payload: str) -> str:
role, _, text = payload.partition("\n---\n")
system = ROLES.get((role or "plain").strip(), ROLES["plain"])
text = (text or payload).strip()
response = _client.models.generate_content(
model="gemini-3.1-flash-lite",
contents=text,
config=types.GenerateContentConfig(
system_instruction=system, max_output_tokens=600,
),
)
return response.text
Project files
.gitignoreREADME.mdbackend/Dockerfilebackend/ai_gemini.pybackend/ai_openai.pybackend/main.pybackend/requirements.txtbootstrap-secrets.shdocker-compose.ymlfrontend/Dockerfilefrontend/app/layout.tsxfrontend/app/page.tsxfrontend/next.config.tsfrontend/package.jsonfrontend/tsconfig.json