Course EN
← back to chapter

Matemática de calendario

Pregunta lo que sea sobre fechas — "what weekday is Christmas 2027?", "how many

Showcase — Matemática de calendario

Pregunta lo que sea sobre fechas — "what weekday is Christmas 2027?", "how many days until 2026-12-31?" — y el modelo llama a una tool date_facts en lugar de adivinar. La aritmética de día de la semana y de conteo de días es una debilidad conocida del modelo: hacen pattern-match de calendarios en lugar de contar. El datetime de Python sí cuenta. Todo el trabajo del modelo aquí es extraer YYYY-MM-DD de tu oración y narrar los hechos exactos que regresan.

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:

PROVIDER=gemini docker compose up --build

Qué hay aquí

  • backend/ai_openai.pydate_facts (día de la semana, días desde hoy, semana ISO, año bisiesto) + el schema de la tool y el round trip
  • backend/ai_gemini.py — misma tool, la forma FunctionDeclaration de Gemini
  • backend/main.py — loader de FastAPI idéntico; lee PROVIDER y despacha
  • frontend/app/page.tsx — textarea + resultado
  • docker-compose.yml — dos servicios, secrets montados desde ./secrets/

Si una pregunta nombra dos fechas, el modelo emite dos function calls en el mismo turno y ambos resultados regresan juntos — el round trip maneja una lista, no una sola llamada.

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 calendar-math.zip

unzip calendar-math.zip
cd calendar-math
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 2 (OpenAI): calendar math the model can't fake.

Models are famously unreliable at weekday and date-difference arithmetic —
they pattern-match instead of counting. One date_facts tool built on
Python's datetime fixes the whole category: the model extracts the date,
the standard library does the calendar.
"""
import datetime as dt
import json

from openai import OpenAI

_client = OpenAI()

_MODEL = "gpt-5.4-nano"


def _date_facts(date: str) -> dict:
    d = dt.date.fromisoformat(date)
    today = dt.date.today()
    return {
        "date": d.isoformat(),
        "weekday": d.strftime("%A"),
        "days_from_today": (d - today).days,
        "iso_week": d.isocalendar().week,
        "day_of_year": d.timetuple().tm_yday,
        "is_leap_year": (d.year % 4 == 0 and d.year % 100 != 0) or d.year % 400 == 0,
        "today": today.isoformat(),
    }


_TOOLS = [{
    "type": "function",
    "name": "date_facts",
    "description": "Exact calendar facts for a date: weekday, days from today "
                   "(negative if past), ISO week, day of year, leap year. "
                   "Call it once per date mentioned.",
    "parameters": {
        "type": "object",
        "properties": {
            "date": {"type": "string", "description": "The date in YYYY-MM-DD format."},
        },
        "required": ["date"],
    },
}]


def run(question: str) -> str:
    response = _client.responses.create(
        model=_MODEL,
        instructions="Answer calendar questions. Always use the date_facts tool "
                     "for weekday and day-count math; never count days 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

    outputs = []
    for call in calls:
        args = json.loads(call.arguments)
        try:
            result = _date_facts(**args)
        except ValueError 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 2 (Gemini): calendar math the model can't fake."""
import datetime as dt
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"


def _date_facts(date: str) -> dict:
    d = dt.date.fromisoformat(date)
    today = dt.date.today()
    return {
        "date": d.isoformat(),
        "weekday": d.strftime("%A"),
        "days_from_today": (d - today).days,
        "iso_week": d.isocalendar().week,
        "day_of_year": d.timetuple().tm_yday,
        "is_leap_year": (d.year % 4 == 0 and d.year % 100 != 0) or d.year % 400 == 0,
        "today": today.isoformat(),
    }


_CONFIG = types.GenerateContentConfig(
    system_instruction="Answer calendar questions. Always use the date_facts tool "
                       "for weekday and day-count math; never count days yourself.",
    tools=[types.Tool(function_declarations=[types.FunctionDeclaration(
        name="date_facts",
        description="Exact calendar facts for a date: weekday, days from today "
                    "(negative if past), ISO week, day of year, leap year. "
                    "Call it once per date mentioned.",
        parameters=types.Schema(
            type=types.Type.OBJECT,
            properties={
                "date": types.Schema(type=types.Type.STRING,
                                     description="The date in YYYY-MM-DD format."),
            },
            required=["date"],
        ),
    )])],
)


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 = _date_facts(**fc.args)
        except ValueError 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

  • .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