Unit converter
Ask a conversion question in plain language — "how many cups is 1.5 liters?",
Showcase — Unit converter
Ask a conversion question in plain language — "how many cups is 1.5 liters?",
"425°F in Celsius?" — and the model turns it into a convert function call.
Exact Python arithmetic produces the number; the model only decides what to
convert and writes the sentence around the result. Length, mass, volume, and
temperature families, with the offset math for temperature handled in code.
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 conversion tables, the tool schema, and the two-call round trip (function_callout,function_call_outputback)backend/ai_gemini.py— same function, declared with typedSchemaobjectsbackend/main.py— identical FastAPI loader; reads PROVIDER and dispatchesfrontend/app/page.tsx— textarea + resultdocker-compose.yml— two services, secrets mounted from./secrets/
run(input: str) -> str executes at most one tool round trip: if the model
doesn't ask for a conversion, its plain answer comes straight back.
Stop
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.
unzip unit-converter.zip
cd unit-converter
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 5 - Showcase 1 (OpenAI): natural-language unit conversion.
The model reads the question and decides what to convert; plain Python
does the arithmetic. One tool, one round trip: function call out, exact
result back in, grounded answer out.
"""
import json
from openai import OpenAI
_client = OpenAI()
_MODEL = "gpt-5.4-nano"
# Exact conversion tables — the part the model is bad at and Python isn't.
_TO_BASE = {
"length_m": {"mm": 0.001, "cm": 0.01, "meters": 1.0, "km": 1000.0,
"inches": 0.0254, "feet": 0.3048, "yards": 0.9144, "miles": 1609.344},
"mass_kg": {"grams": 0.001, "kg": 1.0, "ounces": 0.028349523125, "pounds": 0.45359237},
"volume_l": {"ml": 0.001, "liters": 1.0, "cups": 0.2365882365,
"pints": 0.473176473, "gallons": 3.785411784},
}
_UNITS = sorted({u for table in _TO_BASE.values() for u in table} | {"celsius", "fahrenheit", "kelvin"})
def _convert(value: float, from_unit: str, to_unit: str) -> float:
# Temperature is offset-based, not factor-based — handle it apart.
temps = {"celsius", "fahrenheit", "kelvin"}
if from_unit in temps or to_unit in temps:
c = {"celsius": lambda v: v,
"fahrenheit": lambda v: (v - 32) * 5 / 9,
"kelvin": lambda v: v - 273.15}[from_unit](value)
out = {"celsius": c, "fahrenheit": c * 9 / 5 + 32, "kelvin": c + 273.15}[to_unit]
return round(out, 4)
for table in _TO_BASE.values():
if from_unit in table and to_unit in table:
return round(value * table[from_unit] / table[to_unit], 6)
raise ValueError(f"cannot convert {from_unit} to {to_unit}")
_TOOLS = [{
"type": "function",
"name": "convert",
"description": "Convert a value between measurement units exactly. "
"Units must belong to the same family (length, mass, volume, temperature).",
"parameters": {
"type": "object",
"properties": {
"value": {"type": "number"},
"from_unit": {"type": "string", "enum": _UNITS},
"to_unit": {"type": "string", "enum": _UNITS},
},
"required": ["value", "from_unit", "to_unit"],
},
}]
def run(question: str) -> str:
response = _client.responses.create(
model=_MODEL,
instructions="Answer unit-conversion questions. Always use the convert "
"tool for the math; never compute conversions yourself.",
input=question.strip(),
tools=_TOOLS,
)
calls = [item for item in response.output if item.type == "function_call"]
if not calls:
return response.output_text # nothing to convert; the model said so
outputs = []
for call in calls:
args = json.loads(call.arguments)
try:
result = {"result": _convert(**args)}
except (ValueError, KeyError) as exc:
result = {"error": str(exc)}
outputs.append({
"type": "function_call_output",
"call_id": call.call_id,
"output": json.dumps(result),
})
final = _client.responses.create(
model=_MODEL,
previous_response_id=response.id,
input=outputs,
tools=_TOOLS,
)
return final.output_text
backend/ai_gemini.py
"""Week 5 - Showcase 1 (Gemini): natural-language unit conversion."""
import os
from google import genai
from google.genai import types
_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
_MODEL = "gemini-3.1-flash-lite"
_TO_BASE = {
"length_m": {"mm": 0.001, "cm": 0.01, "meters": 1.0, "km": 1000.0,
"inches": 0.0254, "feet": 0.3048, "yards": 0.9144, "miles": 1609.344},
"mass_kg": {"grams": 0.001, "kg": 1.0, "ounces": 0.028349523125, "pounds": 0.45359237},
"volume_l": {"ml": 0.001, "liters": 1.0, "cups": 0.2365882365,
"pints": 0.473176473, "gallons": 3.785411784},
}
_UNITS = sorted({u for table in _TO_BASE.values() for u in table} | {"celsius", "fahrenheit", "kelvin"})
def _convert(value: float, from_unit: str, to_unit: str) -> float:
temps = {"celsius", "fahrenheit", "kelvin"}
if from_unit in temps or to_unit in temps:
c = {"celsius": lambda v: v,
"fahrenheit": lambda v: (v - 32) * 5 / 9,
"kelvin": lambda v: v - 273.15}[from_unit](value)
out = {"celsius": c, "fahrenheit": c * 9 / 5 + 32, "kelvin": c + 273.15}[to_unit]
return round(out, 4)
for table in _TO_BASE.values():
if from_unit in table and to_unit in table:
return round(value * table[from_unit] / table[to_unit], 6)
raise ValueError(f"cannot convert {from_unit} to {to_unit}")
_CONFIG = types.GenerateContentConfig(
system_instruction="Answer unit-conversion questions. Always use the convert "
"tool for the math; never compute conversions yourself.",
tools=[types.Tool(function_declarations=[types.FunctionDeclaration(
name="convert",
description="Convert a value between measurement units exactly. "
"Units must belong to the same family (length, mass, volume, temperature).",
parameters=types.Schema(
type=types.Type.OBJECT,
properties={
"value": types.Schema(type=types.Type.NUMBER),
"from_unit": types.Schema(type=types.Type.STRING, enum=_UNITS),
"to_unit": types.Schema(type=types.Type.STRING, enum=_UNITS),
},
required=["value", "from_unit", "to_unit"],
),
)])],
)
def run(question: str) -> str:
contents = [types.Content(role="user", parts=[types.Part(text=question.strip())])]
response = _client.models.generate_content(
model=_MODEL, contents=contents, config=_CONFIG,
)
if not response.function_calls:
return response.text or ""
contents.append(response.candidates[0].content)
parts = []
for fc in response.function_calls:
try:
result = {"result": _convert(**fc.args)}
except (ValueError, KeyError) as exc:
result = {"error": str(exc)}
parts.append(types.Part.from_function_response(name=fc.name, response=result))
contents.append(types.Content(role="user", parts=parts))
final = _client.models.generate_content(
model=_MODEL, contents=contents, config=_CONFIG,
)
return final.text or ""
Archivos del proyecto
.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