JevCode / Ökosystem-Beispiele

Datenerfassung

Extrahiert absolute und relative Daten, indem es TypeSafe nach den in einem Dokument benannten Teilen fragt und diese anschließend im Code durch auflösen und validieren lässt, wobei eine vertrauensbasierte Überprüfung erfolgt.

Maschinell aus en übersetzt, nicht lektoriert. Nur als Kurzreferenz geeignet.

Quelle: docs.typesafe.ai/cookbooks/date_extraction_cookbookcookbookrecipe
Extracting three dates from a calendar grid

Lies die Teile eines Datums aus dem Text mit TypeSafe ab und löse sie dann in Code zu einem date auf.

Die Funktion, die Sie hier erstellen, extract_date(document, role), nimmt ein Dokument und eine Phrase, die das gewünschte Datum benennt, wie zum Beispiel „die Frist zur Rückgabe des Formulars“, und gibt ein date mit einer Konfidenz zurück. Sie markiert eine Lesung mit niedriger Konfidenz sowie eine, deren Teile überhaupt kein Datum ergeben, einschließlich eines Datums, das das Dokument niemals angibt. Das Datum kann ausgeschrieben sein („14. August 2027“) oder relativ zu heute geschrieben sein („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. Code wandelt diese Antworten in ein date um. Das Modell liest, was der Text sagt, und führt niemals Kalenderberechnungen durch.

Die folgenden Zellen führen diese Funktion über vier kurze Dokumente aus, geben jedes Datum zusammen mit seiner Konfidenz aus und teilen die Ergebnisse in die Kategorien auf, die Code akzeptiert, und die, die eine Person prüfen sollte.

Overview diagram

TypeSafe liest, wie das Datum geschrieben ist und welche Teile der Text nennt. Code wandelt diese Antworten in ein date um, zählt ab heute, wenn das Datum relativ ist, und akzeptiert es entweder oder sendet es zur Prüfung.

Einrichtung

pip install ipython "typesafe-sdk>=0.5.7" cooksafe --extra-index-url https://pypi.typesafe.ai/

dann TYPESAFE_API_KEY festlegen.

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 werden in einem Aufruf gesendet. mode legt fest, wie das Datum geschrieben wird: absolute für ein Datum, das einen Monat benennt, relative für eines, das relativ zu heute geschrieben ist, und none wenn das Dokument das Datum überhaupt nicht angibt.

Die anderen sechs lesen die Teile. Ein absolutes Datum benötigt month, day und year. Ein relatives benötigt day_anchor: heute, morgen, übermorgen oder einen benannten Wochentag. Wenn es einen Wochentag nennt, sagen weekday und week_offset, welcher und welche Woche. Der Code liest nur die Teile, die mode anfordert.

year listet eine Option pro Jahr von 1900 bis 2050 sowie zwei Ausweichmöglichkeiten. none bedeutet, dass der Text kein Jahr angibt und der Code eines einfügt. out_of_range bedeutet, dass der Text ein Jahr angibt, das nicht in der Liste enthalten ist, und der Code dies anstelle eines Raten meldet. Wenn eine so lange Liste störend wirkt, ziehen Sie zunächst die jahresartigen Zahlen aus dem Text heraus und bieten Sie 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},
        ),
    }

Löse es im Code

read_parts trifft die Entscheidung. assemble wandelt die Antworten in ein date um: Es ergänzt das Jahr, wenn der Text keines angibt, und ermittelt, auf welchen Tag ein genannter Wochentag verweist. Beide zählen von TODAY aus, das festgelegt ist, damit relative Datumsangaben bei jeder Ausführung gleich herauskommen. assemble meldet zudem das niedrigste Konfidenzniveau unter den verwendeten Teilen, sodass eine schwache Antwort in einem Teil das gesamte Datum zur Überprüfung markieren kann.

„next Thursday“ kann zwei verschiedene Tage bedeuten, daher entscheidet der Code, welcher gemeint ist. Ein Wochentag ohne Zusatz bedeutet den nächsten, der auf oder nach heute folgt. 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))

Führe es aus

Sechs Fragen in vier kurzen Dokumenten: zwei Daten aus einem Vertrag, der seine Laufzeit angibt, eine Frist für ein Formular ohne Jahresangabe, eine Umfrage, die „heute“ endet, eine Bewertung für „nächsten Donnerstag“ und ein Datum, das das Formular nie erwähnt. Alle lösen sich gegenüber TODAY = 2026-07-30, einem Donnerstag, auf.

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 gibt beide Jahre an, daher wurden diese aus dem Text entfernt. Das Formular gibt kein Jahr an, also hat der Code 2026 eingetragen: Er nimmt das aktuelle Jahr und wechselt erst zum nächsten, wenn das Datum bereits mehr als einen Monat zurückliegt. „Heute“ und „nächsten Donnerstag“ durchliefen die gleiche Funktion wie die ausgeschriebenen Datumsangaben.

Der Kickoff-Anruf ist derjenige, den das Formular nie erwähnt. In diesem Formular steht ein Datum, nur nicht in diesem hier, und die Notiz absolute date incomplete bedeutet, dass mode mit absolute zurückkam, ohne einen passenden Monat. Das Datum kam leer zurück, die Konfidenz beträgt 0,46 und die Zeile wurde für eine Person markiert.

Vertrauen, um darauf zu routen

Jede Antwort wird mit einem kalibrierten Konfidenzwert zurückgegeben, und das Konfidenzniveau eines Datums ist der niedrigste Wert unter den Teilen, die in es eingeflossen sind. Ein Datum unter REVIEW_BELOW = 0,60 wird einer Person zugeleitet, und dasselbe gilt für ein Datum, das der Code nicht zusammenstellen konnte. Der Rest wird direkt durchgelassen.

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)

Öffnen Sie es im TypeSafe-Playground

Der untenstehende Link enthält die „nächsten Donnerstag“-Nachricht und dieselben Fragen, die der Code sendet. Öffnen Sie ihn, um die Antworten und ihre Konfidenzwerte einzusehen sowie die Formulierung 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})"
        )
    )

Dieses Dokument + Fragen im TypeSafe-Playground öffnen →