Course ES
← back to chapter

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 — the ROLES map (the lesson) + the OpenAI call
  • backend/ai_gemini.py — same ROLES map, Gemini call
  • backend/main.py — identical FastAPI loader; reads PROVIDER and dispatches
  • frontend/app/page.tsx — textarea + role <select> + result
  • docker-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.

Download role-control.zip

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

  • .gitignore
  • README.md
  • backend/Dockerfile
  • backend/ai_gemini.py
  • backend/ai_openai.py
  • backend/main.py
  • backend/requirements.txt
  • bootstrap-secrets.sh
  • docker-compose.yml
  • frontend/Dockerfile
  • frontend/app/layout.tsx
  • frontend/app/page.tsx
  • frontend/next.config.ts
  • frontend/package.json
  • frontend/tsconfig.json