Course EN
← back to chapter

Control de rol

Haz una pregunta, luego cambia el rol detrás de ella — maestro llano,

Showcase — Control de rol

Haz una pregunta, luego cambia el rol detrás de ella — maestro llano, ingeniero escéptico, explícalo-a-un-niño-de-nueve-años, lista con viñetas. La pregunta se queda igual. Solo cambia el system prompt, y la respuesta cambia con él.

Córrelo

bash bootstrap-secrets.sh              # reads ../../../../.env, writes secrets/
docker compose up --build              # default: PROVIDER=openai

Abre http://localhost:3000.

Para correrlo contra Gemini en su lugar:

PROVIDER=gemini docker compose up --build

Qué hay aquí

  • backend/ai_openai.py — el mapa ROLES (la lección) + la llamada a OpenAI
  • backend/ai_gemini.py — el mismo mapa ROLES, llamada a Gemini
  • backend/main.py — loader de FastAPI idéntico; lee PROVIDER y despacha
  • frontend/app/page.tsx — textarea + <select> de rol + resultado
  • docker-compose.yml — dos servicios, secrets montados desde ./secrets/

El frontend codifica <role>\n---\n<question> en el input de un solo string que el backend espera, lo que mantiene el contrato def run(input: str) -> str idéntico en cada showcase de esta semana.

Detenlo

docker compose down

Ejecútalo en tu máquina

Descarga el proyecto como ZIP y córrelo con Docker. Levanta un backend FastAPI y un frontend Next.js en localhost:3000.

Descargar 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

Escribe algo, elige un proveedor y ejecuta el mismo código de Código contra la API real. Requiere iniciar sesión.


  

Los mismos módulos que ejecuta el botón Run. El proyecto completo (frontend, Dockerfile, compose) está en el ZIP, pestaña 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

Archivos del proyecto

  • .gitignore
  • README.es.md
  • 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