Datumsextraktion
Extrahiert absolute und relative Datumsangaben, indem TypeSafe nach den im Dokument genannten Teilen gefragt wird; der Code setzt sie zusammen und prüft sie mit konfidenzbasierter Nachprüfung.
Lies die Teile eines Datums mit TypeSafe aus dem Text und setze sie im Code zu einem date zusammen.
Die Funktion, die du hier baust, extract_date(document, role), nimmt ein Dokument und
eine Phrase, die das gewünschte Datum benennt, etwa „die Frist zur Rücksendung des
Formulars“, und gibt ein date samt Konfidenz zurück. Sie markiert einen konfidenzarmen
Lesevorgang und einen, dessen Teile sich gar nicht zu einem Datum zusammensetzen, auch ein
Datum, das das Dokument nie nennt. Das Datum kann ausgeschrieben sein („14. August 2027“)
oder relativ zu heute geschrieben („morgen“, „nächsten Donnerstag“).
TypeSafe beantwortet Choice-Fragen zum Datum in einem Aufruf: welche Art von Datum es
ist und welchen Monat, Tag, Jahr oder Wochentag der Text nennt. Der Code verwandelt diese
Antworten in ein date. Das Modell liest, was der Text sagt, und rechnet nie selbst am
Kalender.
Die Zellen unten führen diese Funktion über vier kurze Dokumente aus, geben jedes Datum mit seiner Konfidenz aus und teilen die Ergebnisse in die auf, die der Code akzeptiert, und die, die ein Mensch prüfen sollte.
TypeSafe liest, wie das Datum geschrieben ist und welche Teile der Text nennt. Der Code
verwandelt diese Antworten in ein date, rechnet bei einem relativen Datum von heute aus
und akzeptiert es entweder oder schickt es zur Prüfung.
Einrichtung
pip install ipython 'cooksafe>=0.2.0,<0.3.0'
Lege dann TYPESAFE_API_KEY fest.
import os
from datetime import date, timedelta
from pathlib import Path
from cooksafe import JsonCache, make_playground_link
from IPython.display import Markdown, display
from typesafe_sdk import Choice, TypeSafeClient
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
# Cached to json_cache.json (shipped with the cookbook, so re-rendering replays the published
# results with no API spend); delete it to re-run live.
json_cache = JsonCache(Path("json_cache.json"))
# The demo cells below run when this file is executed as the cookbook; the constants and the pure
# resolve/assemble code stay importable, so the calendar math can be unit-tested on its own.
if __name__ == "__cookbook__":
client = TypeSafeClient(
api_key=os.environ.get(
"TYPESAFE_API_KEY", "cache-only"
), # cached re-renders need no key
base_url=os.environ.get("TYPESAFE_BASE_URL"),
timeout=30.0,
)
Die Fragen
Sieben Choice-Fragen gehen in einem Aufruf hinaus. mode sagt, wie das Datum
geschrieben ist: absolute für ein Datum, das einen Monat nennt, relative für eines, das
relativ zu heute geschrieben ist, und none, wenn das Dokument das Datum gar nicht
angibt.
Die anderen sechs lesen die Teile. Ein absolutes Datum braucht month, day und year.
Ein relatives braucht day_anchor: heute, morgen, den Tag danach oder einen benannten
Wochentag. Wenn es einen Wochentag nennt, sagen weekday und week_offset, welchen und
welche Woche. Der Code liest nur die Teile, die mode verlangt.
year listet eine Option pro Jahr von 1900 bis 2050 auf, plus zwei Auswege. none
bedeutet, der Text nennt kein Jahr und der Code trägt eines ein. out_of_range bedeutet,
der Text nennt ein Jahr außerhalb der Liste, und der Code markiert das, statt zu raten.
Wenn dich eine so lange Liste stört, zieh die jahresartigen Zahlen zuerst aus dem Text und
biete dem Modell nur diese an.
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},
),
}
Im Code auflösen
read_parts macht den Aufruf. assemble verwandelt die Antworten in ein date: Es trägt
das Jahr ein, wenn der Text keines nennt, und ermittelt, auf welchen Tag ein benannter
Wochentag zeigt. Beides rechnet von TODAY aus, das festgelegt ist, damit relative
Datumsangaben bei jedem Durchlauf gleich herauskommen. assemble meldet außerdem die
niedrigste Konfidenz unter den verwendeten Teilen, sodass eine schwache Antwort bei einem
einzelnen Teil das ganze Datum zur Prüfung schicken kann.
„nächsten Donnerstag“ kann zwei verschiedene Tage bedeuten, also entscheidet der Code,
welchen. Ein Wochentag ohne Zusatz bedeutet den nächsten ab heute. next bedeutet die
folgende Kalenderwoche, und current bedeutet diese Woche.
@json_cache
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))
Ausführen
Sechs Fragen über vier kurze Dokumente: zwei Datumsangaben aus einem Vertrag, der seine
Jahre nennt, eine Frist in einem Formular ohne Jahr, eine Umfrage, die „heute“ schließt,
ein Review, angesetzt für „nächsten Donnerstag“, und ein Datum, das das Formular nie
erwähnt. Alle lösen sich gegen TODAY = 2026-07-30 auf, einen Donnerstag.
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."
# (document, question phrase, expected date) -- the expected value is only for the scorecard.
EXAMPLES = [
(CONTRACT, "the date the agreement takes effect", date(2025, 1, 1)),
(CONTRACT, "the date the agreement expires", date(2027, 12, 31)),
(FORM, "the deadline to return the form", date(2026, 8, 14)),
(FORM, "the date of the kickoff call", None),
(SURVEY, "the date the survey closes", date(2026, 7, 30)),
(REVIEW, "the date of the design review", date(2026, 8, 6)),
]
if __name__ == "__cookbook__":
print(f"{'':3}{'question':<38}{'expected':<12}{'got':<12}{'conf':>6} flags")
print("-" * 84)
for document, role, expected in EXAMPLES:
r = extract_date(document, role)
got = r["date"].isoformat() if r["date"] else "none"
exp = expected.isoformat() if expected else "none"
mark = "OK" if r["date"] == expected else "XX"
conf = f"{r['confidence']:.2f}" if r["confidence"] is not None else " n/a"
flags = " <== review" if r["needs_review"] else ""
if r["note"]:
flags += f" ({r['note']})"
print(f"{mark:<3}{role:<38}{exp:<12}{got:<12}{conf:>6}{flags}")
question expected got conf flags
------------------------------------------------------------------------------------
OK the date the agreement takes effect 2025-01-01 2025-01-01 0.97
OK the date the agreement expires 2027-12-31 2027-12-31 0.91
OK the deadline to return the form 2026-08-14 2026-08-14 0.95
OK the date of the kickoff call none none 0.46 <== review (absolute date incomplete)
OK the date the survey closes 2026-07-30 2026-07-30 0.94
OK the date of the design review 2026-08-06 2026-08-06 0.92
Der Vertrag nennt beide Jahre, also stammen die aus dem Text. Das Formular nennt kein Jahr, also hat der Code 2026 eingetragen: Er nimmt das aktuelle Jahr und geht nur dann zum nächsten über, wenn das Datum bereits mehr als einen Monat zurückliegt. „heute“ und „nächsten Donnerstag“ liefen durch dieselbe Funktion wie die ausgeschriebenen Daten.
Der Kickoff-Call ist der, den das Formular nie erwähnt. Es gibt ein Datum in diesem
Formular, nur nicht dieses, und der Hinweis absolute date incomplete bedeutet, dass
mode als absolute zurückkam, ohne dass ein Monat dazugehörte. Das Datum kam leer
zurück, die Konfidenz liegt bei 0.46, und die Zeile wird für einen Menschen markiert.
Konfidenz für die Weiterleitung
Jede Antwort kommt mit einer kalibrierten Konfidenz zurück, und die Konfidenz eines Datums
ist die niedrigste unter den Teilen, die in es eingegangen sind. Ein Datum unter
REVIEW_BELOW = 0.60 geht an einen Menschen, und ebenso eines, das der Code gar nicht
zusammensetzen konnte. Der Rest geht direkt durch.
if __name__ == "__cookbook__":
confident = [
(doc, role)
for doc, role, _ in EXAMPLES
if not extract_date(doc, role)["needs_review"]
]
review = [
(doc, role)
for doc, role, _ in EXAMPLES
if extract_date(doc, role)["needs_review"]
]
print(f"auto-accept ({len(confident)}):")
for _doc, role in confident:
print(f" - {role}")
print(f"\nsend to review ({len(review)}):")
for _doc, role in review:
r = extract_date(_doc, role)
print(
f" - {role} (conf {r['confidence']:.2f} / {r['note'] or 'low confidence'})"
)
auto-accept (5):
- the date the agreement takes effect
- the date the agreement expires
- the deadline to return the form
- the date the survey closes
- the date of the design review
send to review (1):
- the date of the kickoff call (conf 0.46 / absolute date incomplete)
Im TypeSafe-Playground öffnen
Der Link unten trägt die Nachricht „nächsten Donnerstag“ und dieselben Fragen, die der Code sendet. Öffne ihn, um die Antworten und ihre Konfidenzen zu sehen und den Wortlaut zu ändern, ohne Code zu schreiben.
if __name__ == "__cookbook__":
playground_link = make_playground_link(
REVIEW, date_questions("the date of the design review"), models=[TYPESAFE_MODEL]
)
display(
Markdown(
f"🔗 [Open this document + questions in the TypeSafe playground]({playground_link})"
)
)
Öffne dieses Dokument + die Fragen im TypeSafe-Playground →