Cookbook: Date extraction
TL;DR Ask seven
Choicequestions in one request (mode,month,day,year,day_anchor,weekday,week_offset) that read only what the text says. Code turns the answers into adate— filling in a missing year, resolving "next Thursday" against a pinnedTODAY. The date's confidence is the minimum across the parts actually used; belowREVIEW_BELOW = 0.60, or if the parts don't assemble, send it to a human. The model never does calendar arithmetic.
Goal
Build extract_date(document, role): given a document and a phrase naming which date you want ("the deadline to return the form"), return a date plus a confidence. It has to handle spelled-out dates ("August 14, 2027"), relative dates ("tomorrow", "next Thursday"), and the case where the document never states the date at all.
The split of labour is the point: "TypeSafe answers Choice questions about the date in one call: what kind of date it is, and which month, day, year, or weekday the text names. Code turns those answers into a date. The model reads what the text says and never does the calendar math."
Inputs / state shape
The state is the raw document string — not a dict:
client.system_one(state=document, questions=date_questions(role), model=TYPESAFE_MODEL)
The role phrase is interpolated into every question's instructions, so the same document can be queried for several different dates.
Constants, verbatim:
TYPESAFE_MODEL = "jev-1.12"
TODAY = date(2026, 7, 30) # fixed reference "today" so relative dates resolve reproducibly
REVIEW_BELOW = 0.60 # gate: a date below this confidence is flagged for a human
MONTHS = {"January": 1, "February": 2, "March": 3, "April": 4, "May": 5, "June": 6,
"July": 7, "August": 8, "September": 9, "October": 10, "November": 11, "December": 12}
WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
YEAR_WINDOW = list(range(1900, 2051)) # 1900..2050
The four demo documents:
CONTRACT = "This agreement is effective January 1, 2025 and expires December 31, 2027."
FORM = "Please return the signed form by August 14."
SURVEY = "Heads up - the customer survey closes today at 5pm."
REVIEW = "Let's schedule the design review for next Thursday."
Questions asked
All seven, verbatim. Note the shared absent string used as the none option's description:
def date_questions(role: str) -> dict[str, Choice]:
"""Seven typed choices that read a date's shape and parts off the text -- no math."""
absent = "The document does not state this, or it is not this kind of date."
return {
"mode": Choice(
instructions=(
f"How is {role} written? 'absolute' = a calendar date naming a month (e.g. "
"'August 14', 'the 3rd of March'); 'relative' = given relative to today (today, "
"tomorrow, the day after tomorrow, or a named weekday such as 'next Thursday'); "
"'none' = the document does not state this date."
),
criteria={"absolute": None, "relative": None, "none": None},
),
"month": Choice(
instructions=f"If {role} is an absolute calendar date, which month is it in?",
criteria={m: None for m in MONTHS} | {"none": absent},
),
"day": Choice(
instructions=f"If {role} is an absolute calendar date, which day of the month (1-31)?",
criteria={str(d): None for d in range(1, 32)} | {"none": absent},
),
"year": Choice(
instructions=(
f"If {role} is an absolute calendar date, which year? Pick 'none' if the document "
"states no year (code infers it), or 'out_of_range' if a year is stated but not "
"in the list."
),
criteria={str(y): None for y in YEAR_WINDOW}
| {
"out_of_range": "A year is stated for this date but is outside the listed range.",
"none": "No year is stated for this date.",
},
),
"day_anchor": Choice(
instructions=(
f"If {role} is relative to today, which day is it? 'today', 'tomorrow', "
"'day_after' (the day after tomorrow), or 'weekday' (a named day of the week)."
),
criteria={"today": None, "tomorrow": None, "day_after": None,
"weekday": None, "none": absent},
),
"weekday": Choice(
instructions=f"If {role} names a day of the week, which one?",
criteria={w: None for w in WEEKDAYS} | {"none": absent},
),
"week_offset": Choice(
instructions=(
f"If {role} names a weekday, which week is it in? 'next' for 'next Thursday' or "
"'Thursday next week'; 'current' for 'this Thursday'; 'none' for a bare weekday "
"with no qualifier (just 'Thursday' / 'on Thursday')."
),
criteria={"current": None, "next": None, "none": absent},
),
}
criteria values of None mean the option name carries the whole meaning — no description needed. Choice.criteria is a dict in both 0.5.7 and 0.6.0; only Score.criteria changed.
The cookbook's note on the 151-option year question: "year lists one option per year from 1900 to 2050, plus two escapes. none means the text states no year and code fills one in. out_of_range means the text states a year outside the list, and code flags that instead of guessing. If a list that long bothers you, pull the year-like numbers out of the text first and offer the model only those."
Combining logic in code
import os
from datetime import date, timedelta
from typesafe_sdk import Choice, TypeSafeClient
client = TypeSafeClient(
api_key=os.environ.get("TYPESAFE_API_KEY", "cache-only"),
base_url=os.environ.get("TYPESAFE_BASE_URL"),
timeout=30.0,
)
def read_parts(document: str, role: str) -> dict:
"""One TypeSafe call -> {part: {choice, confidence}} for the seven questions."""
answers = client.system_one(
state=document, questions=date_questions(role), model=TYPESAFE_MODEL
).answers
return {
part: {"choice": ans.choice, "confidence": ans.confidence}
for part, ans in answers.items()
}
def resolve_weekday(today: date, weekday: str, week_offset: str) -> date:
"""Which date a named weekday points to, by our stated convention: a bare weekday is the next
occurrence on or after today; 'next' is the following calendar week; 'current' is this week."""
w = WEEKDAYS.index(weekday)
this_monday = today - timedelta(days=today.weekday())
if week_offset == "next":
return this_monday + timedelta(days=7 + w)
if week_offset == "current":
return this_monday + timedelta(days=w)
return today + timedelta(days=(w - today.weekday()) % 7)
def assemble(parts: dict, today: date = TODAY) -> dict:
"""Resolve the parts TypeSafe read into a concrete date, in code. Confidence is the weakest of
the parts the shape actually used."""
mode = parts["mode"]["choice"]
confs = [parts["mode"]["confidence"]]
def result(resolved: date | None, note: str) -> dict:
usable = [c for c in confs if c is not None]
confidence = min(usable) if usable else None
needs_review = resolved is None or confidence is None or confidence < REVIEW_BELOW
return {"date": resolved, "confidence": confidence,
"needs_review": needs_review, "note": note}
if mode == "none":
return result(None, "no such date stated")
if mode == "absolute":
month, day, year = (parts["month"]["choice"], parts["day"]["choice"],
parts["year"]["choice"])
confs += [parts["month"]["confidence"], parts["day"]["confidence"],
parts["year"]["confidence"]]
if "none" in (month, day) or not day.isdigit() or month not in MONTHS:
return result(None, "absolute date incomplete")
if year == "out_of_range": # a year is stated but off the list -> flag, don't guess
return result(None, f"year outside {YEAR_WINDOW[0]}-{YEAR_WINDOW[-1]}")
if year == "none": # no year stated -> infer this year, bumped to next if well past
try:
resolved = date(today.year, MONTHS[month], int(day))
except ValueError: # e.g. February 30 -- an inconsistent read, not a real date
return result(None, f"impossible date: {month} {day}")
if resolved < today - timedelta(days=31):
resolved = date(today.year + 1, MONTHS[month], int(day))
return result(resolved, "")
try: # a stated, in-range year
return result(date(int(year), MONTHS[month], int(day)), "")
except ValueError:
return result(None, f"impossible date: {year}-{month}-{day}")
if mode == "relative":
anchor = parts["day_anchor"]["choice"]
confs.append(parts["day_anchor"]["confidence"])
if anchor == "today":
return result(today, "")
if anchor == "tomorrow":
return result(today + timedelta(days=1), "")
if anchor == "day_after":
return result(today + timedelta(days=2), "")
if anchor == "weekday":
weekday, offset = parts["weekday"]["choice"], parts["week_offset"]["choice"]
confs += [parts["weekday"]["confidence"], parts["week_offset"]["confidence"]]
if weekday not in WEEKDAYS:
return result(None, "relative weekday not read")
return result(resolve_weekday(today, weekday, offset), "")
return result(None, "relative day not read")
return result(None, f"unrecognized mode: {mode}")
def extract_date(document: str, role: str) -> dict:
return assemble(read_parts(document, role))
Three rules live entirely in code and never reach the model: the missing-year rule ("take the current year and move to the next one only when the date is already more than a month past" — the today - timedelta(days=31) test), the weekday convention ("a bare weekday is the next occurrence on or after today; next is the following calendar week; current is this week"), and the min-confidence aggregation.
Results the cookbook reports
Numbers came from jev-1.12, with TODAY pinned to 2026-07-30 (a Thursday). Six extractions across four documents:
| question | expected | got | conf | flags |
|---|---|---|---|---|
| the date the agreement takes effect | 2025-01-01 | 2025-01-01 | 0.97 | |
| the date the agreement expires | 2027-12-31 | 2027-12-31 | 0.91 | |
| the deadline to return the form | 2026-08-14 | 2026-08-14 | 0.95 | |
| the date of the kickoff call | none | none | 0.46 | review (absolute date incomplete) |
| the date the survey closes | 2026-07-30 | 2026-07-30 | 0.94 | |
| the date of the design review | 2026-08-06 | 2026-08-06 | 0.92 |
All six matched the expected value; five auto-accepted, one went to review. Notes from the cookbook: the contract states both of its years, so those came off the text; the form states no year, so code filled in 2026; "today" and "next Thursday" went through the same function as the spelled-out dates.
The kickoff call is the date the form never mentions: "There is a date in that form, just not this one, and the note absolute date incomplete means mode came back absolute with no month to go with it. The date came back empty, the confidence reads 0.46, and the row is flagged for a person."
Adapting it to a new domain
- Change
rolestrings, not the questions. The question set is date-generic; the phrase you pass is how you name which date you want out of a document that may hold several. - Set
TODAY = date.today()in production. The cookbook pins it only for reproducible output. - Shrink
YEAR_WINDOW(or pre-extract year-like tokens with a regex and offer only those) if the 151-option Choice is more than you want to send. - Change the weekday convention by editing
resolve_weekday()— the model'sweek_offsetanswer only reports the qualifier the text used (current/next/none); what those mean is yours. - Adjust
REVIEW_BELOWafter measuring on your own documents;0.60here is a starting point. - Adding a time-of-day would mean more Choice questions of the same shape (hour, minute, am/pm) resolved the same way in code (inferred).
Gotchas
cooksafeis not publicly installable. Install line:pip install ipython "typesafe-sdk>=0.5.7" cooksafe --extra-index-url https://pypi.typesafe.ai/.cooksafeis a TypeSafe helper package on a private index, andpypi.typesafe.aireturned 404 publicly on 2026-09-17. Usepip install typesafe-sdkand reimplement what you need:JsonCache(Path("json_cache.json"))is a decorator that memoizes JSON-serializable return values to a file keyed by call arguments (used as@json_cacheonread_parts);make_playground_link(state, questions, models=[...])builds aconsole.typesafe.ai/playground#share/...URL.base_urlenv var differs across cookbooks. This one readsTYPESAFE_BASE_URL; several others readTYPESAFE_ENDPOINT. Check TYPESAFE_* environment variables across SDKs for what the SDK actually honours.- Irrelevant questions still get answered. Every request asks all seven questions; for an absolute date,
day_anchor/weekday/week_offsetcome back with something.assemble()reads only the partsmodecalls for, and — importantly — only aggregates confidence over those parts. Including irrelevant confidences would poison the gate. - Min-confidence is deliberately pessimistic. "a weak answer on any one part can send the whole date to review."
date()can still raise. February 30 is a perfectly consistent set of part answers and an impossible date; bothtry/except ValueErrorblocks exist for that.__name__ == "__cookbook__"guard. The upstream file runs its demo cells only under the cookbook runner, keeping the constants and pureresolve/assemblecode importable and unit-testable. That guard is a notebook artifact, not SDK behaviour.- Model pinning. Numbers are
jev-1.12;jev-latestnow resolves tojev-1.13.0.
Related
- Cookbooks overview — the cookbook index
- Cookbook: Pre-parsed value extraction — the sibling "parse in code, judge with Jev" recipe
- Choice questions — options,
criteria,confidence - Confidence vs probability — what the gate is actually measuring
- Confidence-gated routing — auto-accept vs. human review
- Speculative fan-out — many questions, one request
- Writing instructions and criteria that Jev reads correctly — wording options the model reads correctly
Sources
- raw/docs/cookbooks__date_extraction_cookbook.md (https://docs.typesafe.ai/cookbooks/date_extraction_cookbook)