---
title: "Cookbook: Pre-parsed value extraction"
type: cookbook
tags: [cookbook, extraction, choice, noul, regex]
created: 2026-09-17
updated: 2026-09-17
confidence: high
sources:
  - raw/docs/cookbooks__pre_parsed_value_extraction_cookbook.md
jev_version: "jev-1.13.0"
sdk_python: "0.6.0"
summary: "A recall-tuned regex finds candidate spans, a Jev Choice over those exact spans picks the one the question asks for, and code copies it verbatim and normalizes it."
---

# Cookbook: Pre-parsed value extraction

> **TL;DR** Make the regex matches the `Choice` options. Because Jev only ever chooses among the spans you hand it, the value you get back is one of those spans copied unchanged — it **cannot invent a value or transpose a digit**. Add a `none` escape hatch, then normalize in code.

## Goal

Extract a verbatim value (an email address, a phone number, an invoice amount) from a document where several candidates exist and only the surrounding words say which one plays the requested role. Three steps:

1. A regex finds the candidate values in the text. **Tune it to over-find.**
2. Jev picks which candidate the question is asking for, and reads off any attribute the code needs downstream (currency, country, whether an amount is a credit or a charge).
3. The code copies the picked value and normalizes it.

You end up with a reusable `find` / `pick` pair plus three worked cases.

## Inputs / state shape

The **state is the raw document string**. Three documents, verbatim:

```python
EMAIL_DOC = """From: Dana Whit <dana.whit@acme-corp.com>
To: billing@acme-corp.com
Cc: orders@acme-corp.com
Reply-To: dana.personal@gmail.com

Hi team - please don't use the billing alias for this one. Send my receipt to my
personal address instead. Thanks, Dana."""

PHONE_DOC = """Reach our San Francisco office at these numbers: main desk (415) 555-0199,
billing fax (415) 555-0142, and my direct cell (415) 555-0177. Call the cell if it's urgent."""

MONEY_DOC = """Invoice INV-2087.
Subtotal: $1,200.00
Sales tax: $115.50
Total due: $1,315.50
A $50.00 courtesy credit from last month has already been applied."""
```

The candidate finders:

```python
EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
PHONE_RE = re.compile(r"\(?\+?\d[\d\s()\-.]{6,}\d")
MONEY_RE = re.compile(r"[$€£¥]\s?\d[\d,]*(?:\.\d{2})?")
```

Constants: `TYPESAFE_MODEL = "jev-1.12"`, `NONE = "none"` — the escape hatch on every selection, "none of the candidates fits".

## Questions asked

Three question shapes, all built dynamically from the candidates.

**`pick` — a `Choice` whose options *are* the regex spans.**

- `type`: `Choice`
- `instructions`: the caller's question, passed through verbatim
- `criteria`: `{candidate: None for candidate in candidates} | {"none": "None of these is the requested value."}`

The option descriptions are `None` because the span itself is the description; the `none` key is the only one carrying text. The exact questions used:

| case | `instructions` |
|---|---|
| email, receipt | `Which email address does the sender want their receipt sent to?` |
| email, sender | `Which email address did this message come from (the From line)?` |
| phone | `Which of these is the direct mobile / cell number?` |
| money, total | `Which amount is the total the customer must pay?` |
| money, credit | `Which amount is the courtesy credit that was applied?` |

**`classify` — a small `Choice` over a fixed label set.**

- `criteria`: `{option: None for option in options}`

| case | `instructions` | options |
|---|---|---|
| phone country | `In what country is this office located?` | `["US", "GB", "DE", "FR", "CA", "AU"]` |
| currency | `What currency are these amounts in?` | `["USD", "EUR", "GBP", "JPY", "CAD"]` |

**`is_true` — a `Noul`**, asked once per picked amount, with the picked span interpolated into the instructions:

```python
f"Is the amount {chosen['choice']} a credit or refund to the customer, not a charge?"
```

## Combining logic in code

```python
import os
import re
from decimal import Decimal

import phonenumbers
from typesafe_sdk import Choice, Noul, TypeSafeClient

TYPESAFE_MODEL = "jev-1.12"
NONE = "none"

ts = TypeSafeClient(
    api_key=os.environ.get("TYPESAFE_API_KEY", "cache-only"),
    base_url=os.environ.get("TYPESAFE_BASE_URL"),
    timeout=30.0,
)


def find(pattern: re.Pattern, text: str) -> list[str]:
    """Code-side candidate finder: recall-tuned regex, deduped, in document order."""
    seen: set[str] = set()
    out: list[str] = []
    for match in pattern.findall(text):
        span = match.strip()
        if span and span not in seen:
            seen.add(span)
            out.append(span)
    return out


def pick(document: str, candidates: list[str], question: str) -> dict:
    """TypeSafe selects which found span plays the role. Returns {choice, confidence}."""
    criteria = {c: None for c in candidates} | {
        NONE: "None of these is the requested value."
    }
    answer = ts.system_one(
        state=document,
        questions={"pick": Choice(instructions=question, criteria=criteria)},
        model=TYPESAFE_MODEL,
    ).answers["pick"]
    return {"choice": answer.choice, "confidence": answer.confidence}


def classify(document: str, question: str, options: list[str]) -> dict:
    """A small Choice over a fixed label set (currency, country, ...)."""
    answer = ts.system_one(
        state=document,
        questions={"q": Choice(instructions=question, criteria={o: None for o in options})},
        model=TYPESAFE_MODEL,
    ).answers["q"]
    return {"choice": answer.choice, "confidence": answer.confidence}


def is_true(document: str, question: str) -> float:
    """A yes/no Noul. Returns P(yes)."""
    return (
        ts.system_one(
            state=document,
            questions={"q": Noul(instructions=question)},
            model=TYPESAFE_MODEL,
        )
        .answers["q"]
        .noul
    )
```

Normalization stays entirely in code. Phone:

```python
phones = find(PHONE_RE, PHONE_DOC)
mobile = pick(PHONE_DOC, phones, "Which of these is the direct mobile / cell number?")
region = classify(PHONE_DOC, "In what country is this office located?",
                  ["US", "GB", "DE", "FR", "CA", "AU"])
parsed = phonenumbers.parse(mobile["choice"], region["choice"])
e164 = phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
```

Money:

```python
def to_decimal(value: str) -> Decimal:
    """Copy the picked value and parse the number in code (US grouping/decimal here)."""
    return Decimal(re.sub(r"[^\d.]", "", value))


for label, chosen in [("total due", total), ("credit", credit)]:
    is_credit = is_true(
        MONEY_DOC,
        f"Is the amount {chosen['choice']} a credit or refund to the customer, not a charge?",
    )
    kind = "credit" if is_credit > 0.5 else "charge"
```

## Results / what the cookbook reports

**Email.** Candidates `['dana.whit@acme-corp.com', 'billing@acme-corp.com', 'orders@acme-corp.com', 'dana.personal@gmail.com']`.

```
receipt -> : dana.personal@gmail.com      (conf 0.98)
sender  -> : dana.whit@acme-corp.com      (conf 1.00)
```

`receipt` is the personal Gmail address on the `Reply-To:` line, which is what the body asks for, not the `To:` billing alias.

**Phone.** Candidates `['(415) 555-0199', '(415) 555-0142', '(415) 555-0177']`.

```
mobile  -> : (415) 555-0177  (conf 1.00)
country -> : US  (conf 0.90)
E.164   -> : +14155550177
```

Nothing in the digits says which number is the mobile or what country it is in; the words around them do.

**Money.** Candidates `['$1,200.00', '$115.50', '$1,315.50', '$50.00']`.

```
total due : $1,315.50  -> 1315.50 USD (charge, P(credit)=0.01)
credit    : $50.00     -> 50.00 USD (credit, P(credit)=0.99)
```

The credit-or-charge `Noul` answers 0.01 on the total and 0.99 on the credit, so the code knows the sign of each `Decimal` it parses.

## Adapting it to a new domain

Point `find`/`pick` at your own documents: write a recall-tuned regex per value type, then one `pick` question per role you need filled, plus `classify` questions for any attribute the normalizer needs (currency, locale, country) and `is_true` questions for any boolean the downstream code branches on. Thresholds are yours — `pick` returns `confidence`, and `none` in `choice` means no candidate fit.

## Gotchas

The cookbook closes with two explicit limits plus one inline warning:

- **A `Choice` question allows at most 255 options.** With more candidates than that, narrow in two stages: pick the section first, then the span inside it.
- **Finding the candidates is the part that takes work.** Emails, phone numbers and amounts have regexes that cover them; a name does not, so its candidates have to come from a roster you already have, from a named-entity recognizer, or from an LLM that proposes them. Jev then picks the one the question asks for.
- **Number formatting is a locale assumption in your code, not Jev's.** `to_decimal` assumes the comma groups thousands and the dot is the decimal point. That holds for `$1,315.50`; in `€1.315,50` it is the other way round. The cookbook's fix: ask a `Noul` which convention the document uses, and branch on it in code.
- **Always include the `none` hatch.** Without it, `Choice` probabilities still sum to 1 and some span wins even when the requested value is absent.
- **Install line.** `pip install ipython phonenumbers "typesafe-sdk>=0.5.7" cooksafe --extra-index-url https://pypi.typesafe.ai/`. `cooksafe` is a helper package from TypeSafe's private index and `pypi.typesafe.ai` returned **404 publicly as of 2026-09-17** — use `pip install typesafe-sdk phonenumbers` and reimplement `JsonCache(Path("json_cache.json"))` (a decorator that memoizes each call's result into a JSON file keyed on its arguments, so cached re-renders make no API calls) and `make_playground_link(state, questions, models=[...])` (builds a `https://console.typesafe.ai/playground#share/...` URL).
- The client here defaults `api_key` to the literal `"cache-only"` so cached re-renders need no key; that is a cookbook convenience, not an API feature.

## Related

- [[concepts/choice]] — options, criteria, and the `confidence` on the pick
- [[concepts/noul]] — the credit-vs-charge check
- [[cookbooks/date-extraction]] — the same "ask for the parts, resolve in code" shape for dates
- [[cookbooks/semantic-find]] — a `Choice` over line ids, with the 255-option limit again
- [[guides/writing-instructions-and-criteria]] — phrasing role-specific questions
- [[cookbooks/overview]] — the full cookbook catalog

## Sources

- raw/docs/cookbooks__pre_parsed_value_extraction_cookbook.md (https://docs.typesafe.ai/cookbooks/pre_parsed_value_extraction_cookbook.md)
