Course ES
← 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

Run locally

Download the project as a ZIP and run it with Docker. Brings up a FastAPI backend + Next.js frontend on localhost:3000.

Download 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

Type some input, pick a provider, and run the same code shown in Source against the live API. Sign-in required.


  

The same modules the Run button hits. The whole project (frontend, Dockerfile, compose) is in the ZIP under 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 ""

Project files

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