Course EN
← back to chapter

Calendar math

Ask anything about dates — "what weekday is Christmas 2027?", "how many days

Showcase — Calendar math

Ask anything about dates — "what weekday is Christmas 2027?", "how many days until 2026-12-31?" — and the model calls a date_facts tool instead of guessing. Weekday and day-count arithmetic is a known model weak spot: they pattern-match calendars instead of counting. Python's datetime counts. The model's whole job here is extracting YYYY-MM-DD from your sentence and narrating the exact facts that come back.

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.pydate_facts (weekday, days from today, ISO week, leap year) + the tool schema and the round trip
  • backend/ai_gemini.py — same tool, Gemini FunctionDeclaration spelling
  • backend/main.py — identical FastAPI loader; reads PROVIDER and dispatches
  • frontend/app/page.tsx — textarea + result
  • docker-compose.yml — two services, secrets mounted from ./secrets/

If a question names two dates, the model emits two function calls in the same turn and both results go back together — the round trip handles a list, not a single call.

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.

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