Form filler
Type an expense the way you'd scribble it on a receipt — "lunch with the
Showcase — Form filler
Type an expense the way you'd scribble it on a receipt — "lunch with the client, 38.50 eur, last tuesday" — and get back a validated record ready to insert: merchant, amount as a real number, currency, a category from your set, and a date. The schema does double duty: it shapes the request and validates the reply.
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— theExpenseschema (typed + Literal category) and the OpenAI callbackend/ai_gemini.py— same schema, Gemini callbackend/main.py— identical FastAPI loader; reads PROVIDER and dispatchesfrontend/app/page.tsx— textarea + result (rendered as JSON)docker-compose.yml— two services, secrets mounted from./secrets/
run(input: str) -> str returns the validated record as pretty JSON, keeping
the contract identical across the week's showcases.
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 form-filler.zip
cd form-filler
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 4 - Showcase 3 (OpenAI): turn a messy note into a validated record.
This is the one closest to real work: a one-line expense note becomes a
structured row ready to insert. The schema does double duty — it tells the
model the shape and it validates the reply, so amount is a number and
category is one of yours, not whatever the model felt like typing.
"""
import json
from typing import Literal
from openai import OpenAI
from pydantic import BaseModel
_client = OpenAI()
class Expense(BaseModel):
merchant: str
amount: float
currency: str
category: Literal["travel", "meals", "software", "hardware", "other"]
date: str
def run(text: str) -> str:
response = _client.responses.parse(
model="gpt-5.4-nano",
input="Turn this expense note into a structured record. Infer the "
"category. Use ISO format for the date if one is given.\n\n"
f"{text.strip()}",
text_format=Expense,
)
return json.dumps(response.output_parsed.model_dump(), indent=2)
backend/ai_gemini.py
"""Week 4 - Showcase 3 (Gemini): turn a messy note into a validated record."""
import json
import os
from typing import Literal
from google import genai
from google.genai import types
from pydantic import BaseModel
_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
class Expense(BaseModel):
merchant: str
amount: float
currency: str
category: Literal["travel", "meals", "software", "hardware", "other"]
date: str
def run(text: str) -> str:
response = _client.models.generate_content(
model="gemini-3.1-flash-lite",
contents="Turn this expense note into a structured record. Infer the "
"category. Use ISO format for the date if one is given.\n\n"
f"{text.strip()}",
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=Expense,
),
)
return json.dumps(response.parsed.model_dump(), indent=2)
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