文档目录
日期提取
从文档中提取绝对日期和相对日期:先用 TypeSafe 读取被点名的日期片段,再在代码里完成解析和校验,并按置信度触发复核。
非官方中文翻译 · 内容以 英文原文 ↗ 为准 · 同步于 2026-10-15
让 TypeSafe 读出文档中日期的各个部分,再在代码里解析成一个 date。
本节构建的函数 extract_date(document, role) 接受一段文档以及一个指明目标日期的短语(例如「the deadline to return the form」),并返回一个附带置信度的 date。它会把低置信度的读数标记出来,也会把根本无法拼成日期的情况标记出来——包括文档根本没提到这个日期的情况。日期可以写成绝对形式(例如「August 14, 2027」),也可以写成相对今天的形式(例如「tomorrow」「next Thursday」)。
TypeSafe 在一次调用中用 Choice(选择题)回答关于日期的若干问题:日期属于哪种类型,以及文本中点出了哪一个月份、日子、年份或星期几。代码再把这些答案拼成一个 date。模型只负责读文本里写了什么,绝不去做日历运算。
下面几个单元格用四段简短文档跑这个函数,打印出每个日期及其置信度,并把结果分成「代码直接接受」和「应交由人工复核」两组。
TypeSafe 读取日期的写法以及文本中点出了哪些部分。代码把这些答案拼成一个 date;若是相对日期,则自今日起算,之后要么直接采纳,要么送交复核。
环境准备
pip install ipython 'cooksafe>=0.2.0,<0.3.0'然后设置 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 负责发起调用。assemble 把答案拼成一个 date:当文本未给出年份时自动补上年份,并算出具名星期几指向哪一天。这两件事都以 TODAY 为基准计数,而 TODAY 被钉死,因此相对日期每次跑出来的结果都一样。assemble 还会报告实际用到的各部分中最低的置信度,因此任何一个部分的弱读数都可能把整个日期送交复核。
「next Thursday」可能指向两个不同的日子,因此由代码来裁决。没有限定语的星期几,指的是自今日起下一个(含今日);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))运行一下
六个问题(question),分布在四份简短的文档中:合同里写明年份的两次日期、一份没写年份的表单截止日期、一份在「今天」关闭的问卷、一场安排在「下周四」的评审,以及表单中从未出现的那条日期。它们全部以 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:先取当前年份,只有当原始日期已经过去一个多月时才顺延到下一年。「今天」和「下周四」走过的是同一条函数路径,和明确写明年份的日期相同。
启动会的日期正是表单中从未提到的那一项。表单里其实有日期,只是不对应这个问题;注释 absolute date incomplete 表明 mode 返回了 absolute,但缺少月份。最终得到的日期为空,置信度为 0.46,这一行被标记转给人工复核。
用置信度来分流
每一条回答都会附带一个校准过的置信度,日期的置信度取自构成它的各个组成部分中最低的那个。置信度低于 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})"
)
)在 TypeSafe playground 中打开此文档和问题 →
本文档由 Mintlify 构建并托管,Mintlify 是一个面向开发者的文档平台。