Extracción de fechas
Extrae fechas absolutas y relativas pidiendo a TypeSafe las partes nombradas en un documento y luego resolviéndolas y validándolas en código con revisión basada en confianza.
Lee las partes de una fecha del texto con TypeSafe y luego resuélvelas a un date en código.
La función que construyes aquí, extract_date(document, role), toma un documento y una frase que nombra la fecha que quieres, como «el plazo para devolver el formulario», y devuelve un date con una confianza. Marca una lectura de baja confianza, y también una cuyas partes no suman una fecha en absoluto, incluida una fecha que el documento nunca menciona. La fecha puede estar escrita con todas sus letras («14 de agosto de 2027») o expresada en relación con hoy («mañana», «el próximo jueves»).
TypeSafe responde a preguntas Choice sobre la fecha en una sola llamada: qué tipo de fecha es, y qué mes, día, año o día de la semana nombra el texto. El código convierte esas respuestas en un date. El modelo lee lo que dice el texto y nunca hace las cuentas del calendario.
Las celdas de abajo ejecutan esa función sobre cuatro documentos cortos, imprimen cada fecha con su confianza y dividen los resultados entre los que acepta el código y los que debería revisar una persona.
TypeSafe lee cómo está escrita la fecha y qué partes nombra el texto. El código convierte esas respuestas en un date, contando desde hoy cuando la fecha es relativa, y o bien la acepta o bien la envía a revisión.
Configuración
pip install ipython 'cooksafe>=0.2.0,<0.3.0'
luego define 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,
)
Las preguntas
En una sola llamada salen siete preguntas Choice. mode dice cómo está escrita la fecha: absolute para una fecha que nombra un mes, relative para una escrita en relación con hoy, y none cuando el documento no menciona la fecha en absoluto.
Las otras seis leen las piezas. Una fecha absoluta necesita month, day y year. Una relativa necesita day_anchor: hoy, mañana, pasado mañana, o un día de la semana con nombre. Cuando nombra un día de la semana, weekday y week_offset dicen cuál y en qué semana. El código lee solo las piezas que pide mode.
year lista una opción por año desde 1900 a 2050, más dos escapes. none significa que el texto no indica ningún año y el código pone uno. out_of_range significa que el texto indica un año fuera de la lista, y el código lo marca en lugar de adivinar. Si una lista tan larga te incomoda, extrae primero del texto los números que parezcan años y ofrece al modelo solo esos.
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},
),
}
Resuélvelo en código
read_parts hace la llamada. assemble convierte las respuestas en un date: rellena el año cuando el texto no indica ninguno, y calcula a qué día apunta un día de la semana con nombre. Ambas cosas cuentan desde TODAY, que está fijado para que las fechas relativas salgan iguales en cada ejecución. assemble también informa de la confianza más baja entre las partes que usó, así que una respuesta débil en una sola parte puede enviar toda la fecha a revisión.
«el próximo jueves» puede significar dos días distintos, así que el código decide cuál. Un día de la semana sin calificador significa el siguiente a partir de hoy. next significa la semana natural siguiente, y current significa esta semana.
@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))
Ejecútalo
Seis preguntas en cuatro documentos cortos: dos fechas de un contrato que indica sus años, un plazo de formulario escrito sin año, una encuesta que cierra «hoy», una revisión fijada para «el próximo jueves» y una fecha que el formulario nunca menciona. Todas se resuelven contra TODAY = 2026-07-30, un jueves.
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
El contrato indica sus dos años, así que salieron del texto. El formulario no indica año, así que el código puso 2026: toma el año actual y pasa al siguiente solo cuando la fecha ya ha pasado hace más de un mes. «hoy» y «el próximo jueves» pasaron por la misma función que las fechas escritas con todas sus letras.
La llamada de inicio es la que el formulario nunca menciona. Hay una fecha en ese formulario, solo que no esta, y la nota absolute date incomplete significa que mode volvió como absolute sin un mes que lo acompañara. La fecha volvió vacía, la confianza marca 0.46 y la fila queda marcada para una persona.
La confianza para enrutar
Cada respuesta vuelve con una confianza calibrada, y la confianza de una fecha es la más baja entre las partes que la componen. Una fecha por debajo de REVIEW_BELOW = 0.60 va a una persona, y también una fecha que el código no pudo ensamblar en absoluto. El resto pasan directamente.
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)
Ábrelo en el playground de TypeSafe
El enlace de abajo lleva el mensaje «el próximo jueves» y las mismas preguntas que envía el código. Ábrelo para ver las respuestas y sus confianzas, y para cambiar la redacción sin escribir nada de código.
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})"
)
)
Abre este documento + las preguntas en el playground de TypeSafe →