Извлечение дат и времени
Extracts absolute and relative dates by asking TypeSafe for the parts named in a document, then resolving and validating them in code with confidence-based review.
Извлекайте абсолютные и относительные даты, запрашивая у TypeSafe указанные в документе компоненты, а затем вычисляйте и валидируйте их в коде с проверкой на основе показателей уверенности (confidence).
Считывайте составные части даты из текста с помощью TypeSafe, а затем преобразуйте их в объект date в программном коде.
Создаваемая здесь функция extract_date(document, role) принимает документ и текстовое описание искомой даты (например, «крайний срок сдачи формы») и возвращает объект date вместе с показателем уверенности. Она отмечает результат с низкой уверенностью, а также случаи, когда компоненты вообще не складываются в корректную дату (включая ситуации, когда дата в документе вовсе не указана). Дата может быть записана явно («14 августа 2027») или относительно сегодняшнего дня («завтра», «в следующий четверг»).
TypeSafe отвечает на вопросы типа Choice о дате за один вызов: как именно записана дата, а также какой месяц, день, год или день недели назван в тексте. Программный код преобразует эти ответы в конкретный объект date. Модель лишь считывает то, что написано в тексте, и никогда не занимается календарной арифметикой.
В ячейках ниже эта функция выполняется для четырех коротких документов, выводит каждую дату с оценкой уверенности и разделяет результаты на принятые кодом и требующие ручной проверки человеком.

TypeSafe определяет, как записана дата и какие ее части названы в тексте. Код превращает эти ответы в объект date, отсчитывая дни от сегодняшнего числа для относительных дат, и либо принимает результат, либо отправляет его на проверку.
Настройка
pip install ipython "typesafe-sdk>=0.5.7" cooksafe --extra-index-url https://pypi.typesafe.ai/
Затем установите переменную TYPESAFE_API_KEY.
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,
)
Вопросы к модели
Семь вопросов Choice отправляются в одном запросе. Вопрос mode определяет, как записана дата: absolute для календарной даты с указанием месяца, relative для даты относительно сегодняшнего дня и none, если документ вообще не содержит дату.
Остальные шесть вопросов считывают конкретные детали. Абсолютной дате требуются month, day и year. Относительной дате требуется day_anchor: сегодня, завтра, послезавтра или именованный день недели. Если указан день недели, вопросы weekday и week_offset определяют, какой именно день и на какой неделе имеется в виду. Код считывает только те части, которые требуются выбранным mode.
Вопрос year перечисляет по одному варианту на каждый год с 1900 по 2050, плюс два специальных варианта: none означает, что год в тексте не указан (код подставит его сам), а out_of_range — что в тексте указан год вне предложенного диапазона, и код пометит это вместо угадывания. Если столь длинный список вариантов кажется избыточным, можно предварительно извлечь числа, похожие на года, регулярным выражением и предложить модели только их.
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},
),
}
Преобразование в коде
Функция read_parts выполняет вызов API. Функция assemble превращает полученные ответы в объект date: она подставляет год, если он не указан в тексте, и определяет конкретное число для именованного дня недели. Оба вычисления отталкиваются от опорной даты TODAY, которая зафиксирована для воспроизводимости относительных дат при каждом запуске. Кроме того, assemble фиксирует наименьшую уверенность среди использованных компонентов, поэтому ненадежный ответ по любой из частей может отправить всю дату на проверку.
Фраза «в следующий четверг» может трактоваться двояко, поэтому выбор логики остается за кодом. День недели без уточнений означает ближайший такой день, начиная с сегодняшнего. Вариант next означает следующую календарную неделю, а current — текущую неделю.
@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))
Запуск примера
Шесть вопросов по четырем коротким документам: две даты из договора с указанием годов, срок сдачи формы без указания года, опрос, который закрывается «сегодня», встреча по дизайну «в следующий четверг», и дата, о которой в форме нет ни слова. Все они рассчитываются относительно TODAY = 2026-07-30 (четверг).
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
Договор явно указывает оба года, поэтому они взяты прямо из текста. Форма год не указывает, поэтому код подставил 2026: берется текущий год и переносится на следующий только в случае, если дата уже прошла более чем на месяц назад. Даты «сегодня» и «в следующий четверг» прошли через ту же функцию, что и полностью прописанные календарные даты.
Вводный звонок (kickoff call) — единственный случай, о котором в форме вообще ничего не говорится. В форме есть другая дата, но не эта, и пометка absolute date incomplete означает, что mode вернул absolute, но без указания месяца. В результате дата осталась пустой, уверенность составила 0.46, и строка была помечена для ручной проверки человеком.
Маршрутизация по показателю уверенности (confidence)
Каждый ответ сопровождается калиброванным показателем уверенности, и итоговая уверенность даты равна минимальной среди всех использованных компонентов. Дата со значением ниже REVIEW_BELOW = 0.60 направляется человеку, как и дата, которую коду вообще не удалось собрать. Остальные проходят конвейер автоматически.
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)
Открыть в TypeSafe Playground
Ссылка ниже содержит сообщение со фразой «в следующий четверг» и те же вопросы, которые отправляются из кода. Откройте ее, чтобы увидеть ответы модели, показатели уверенности и попробовать изменить формулировки без написания кода.
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})"
)
)