Извлечение предварительно разобранных значений
Uses regexes to find candidate emails, phone numbers, and amounts, then has TypeSafe select the requested span so code can normalize a verbatim value.
Использование регулярных выражений для поиска кандидатов (email, телефоны, денежные суммы) с последующим выбором нужного фрагмента через TypeSafe, позволяющим нормализовать значение дословно без галлюцинаций.
Регулярное выражение находит потенциальные значения, TypeSafe выбирает то, о котором идет речь в вопросе, а программный код копирует его дословно.
Связка функций find и pick, представленная здесь, легко адаптируется под ваши документы. В руководстве разбираются три практических сценария: email-адрес, на который отправитель просит выслать квитанцию, номер телефона в международном формате +14155550177 и итоговая сумма счета 1315.50 USD, распознанная как списание.
TypeSafe выбирает один из переданных ему вариантов, поэтому кандидатов необходимо найти заранее. Регулярное выражение выполняет поиск, TypeSafe делает выбор, а код забирает результат в три шага:
- Регулярное выражение находит в тексте потенциальные значения. Настройте его с упором на полноту (recall), чтобы находить с запасом.
- TypeSafe выбирает, какого именно кандидата требует вопрос, и считывает любые атрибуты, необходимые коду далее (валюта, страна, является ли сумма начислением или списанием).
- Программный код копирует выбранное значение и нормализует его.
Поскольку TypeSafe всегда выбирает строго среди фрагментов, найденных регулярным выражением, возвращаемое значение является одним из этих исходных фрагментов без каких-либо изменений. Модель не способна выдумать значение или случайно переставить цифры местами.

Регулярное выражение находит потенциальные значения в документе, TypeSafe выбирает нужное, а последующий код нормализует его и выполняет целевое действие.
Настройка
pip install ipython phonenumbers "typesafe-sdk>=0.5.7" cooksafe --extra-index-url https://pypi.typesafe.ai/
Затем установите переменную TYPESAFE_API_KEY.
import os
import re
from decimal import Decimal
from pathlib import Path
import phonenumbers
from cooksafe import JsonCache, make_playground_link
from IPython.display import Markdown, display
from typesafe_sdk import Choice, Noul, TypeSafeClient
TYPESAFE_MODEL = "jev-1.12"
NONE = "none" # the escape hatch on every selection: "none of the candidates fits"
# base_url defaults to https://api.typesafe.ai/ ; the env override points at another deployment.
ts = 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,
)
json_cache = JsonCache(Path("json_cache.json"))
Вспомогательные функции
Функция find применяет регулярное выражение с избыточным поиском и удаляет дубликаты. Функция pick — это вопрос типа Choice, вариантами которого выступают фрагменты, найденные функцией find; ответом становится точная копия одного из фрагментов либо none, если ни один вариант не подходит. Функция classify — вопрос Choice по фиксированному набору меток, используемый для определения валюты и страны. Функция is_true — вопрос Noul, определяющий, является ли сумма кредитом (скидкой/возвратом).
Каждый вызов кэшируется в json_cache.json, поэтому повторный запуск не совершает обращений к API.
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})?")
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
@json_cache
def pick(document: str, candidates: list[str], question: str) -> dict:
"""TypeSafe selects which found span plays the role. Returns {choice, confidence}.
The options ARE the candidate spans, so ``choice`` is a verbatim copy of one of them (or the
``none`` hatch) - the model chooses, code owns the string."""
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}
@json_cache
def classify(document: str, question: str, options: list[str]) -> dict:
"""A small Choice over a fixed label set (currency, country, ...). Returns {choice, confidence}."""
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}
@json_cache
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
)
Электронная почта: выбор правильного адреса по роли
В заголовках письма четыре адреса. В тексте отправитель просит выслать квитанцию на личный адрес вместо адреса биллинга из поля To:, поэтому правильный ответ зависит от понимания контекста письма. Задаем два вопроса: на какой адрес отправить квитанцию и с какого адреса отправлено сообщение.
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."""
emails = find(EMAIL_RE, EMAIL_DOC)
receipt = pick(
EMAIL_DOC, emails, "Which email address does the sender want their receipt sent to?"
)
sender = pick(
EMAIL_DOC, emails, "Which email address did this message come from (the From line)?"
)
print("candidates :", emails)
# code copies the picked value verbatim and normalizes (lowercase); it never re-types it
print(
f"receipt -> : {receipt['choice'].lower():<28} (conf {receipt['confidence']:.2f})"
)
print(f"sender -> : {sender['choice'].lower():<28} (conf {sender['confidence']:.2f})")
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 — это личный адрес Gmail из строки Reply-To:, запрошенный в тексте письма; адрес sender взят из поля From. Оба значения получены непосредственно из регулярного выражения и приведены к нижнему регистру кодом.
Телефон: выбор мобильного и нормализация в E.164
В тексте три номера телефона, ни один из которых не содержит код страны. TypeSafe выбирает мобильный номер и определяет страну по контексту; библиотека phonenumbers объединяет эти два ответа в международный формат E.164, начинающийся с + и кода страны.
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."""
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"],
)
# code copies the picked value and normalizes it with the model-supplied country
parsed = phonenumbers.parse(mobile["choice"], region["choice"])
e164 = phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
print("candidates :", phones)
print(f"mobile -> : {mobile['choice']} (conf {mobile['confidence']:.2f})")
print(f"country -> : {region['choice']} (conf {region['confidence']:.2f})")
print(f"E.164 -> : {e164}")
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
Сами цифры ничего не говорят о том, какой номер мобильный и к какой стране он относится — это следует из окружающего текста. TypeSafe считывает эти слова, а phonenumbers форматирует выбранный номер как +14155550177.
Деньги: выбор суммы, определение валюты, различие списания и кредита
В счете указаны четыре денежные суммы. TypeSafe выбирает итоговую сумму к оплате и скидочный кредит, считывает валюту и помечает каждую выбранную сумму как начисление или кредит. Код копирует строки и парсит их в объекты Decimal.
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."""
amounts = find(MONEY_RE, MONEY_DOC)
currency = classify(
MONEY_DOC,
"What currency are these amounts in?",
["USD", "EUR", "GBP", "JPY", "CAD"],
)
total = pick(MONEY_DOC, amounts, "Which amount is the total the customer must pay?")
credit = pick(
MONEY_DOC, amounts, "Which amount is the courtesy credit that was applied?"
)
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"
print(
f"{label:<10}: {chosen['choice']:<10} -> {to_decimal(chosen['choice'])} {currency['choice']} "
f"({kind}, P(credit)={is_credit:.2f})"
)
print("\ncandidates :", amounts)
total due : $1,315.50 -> 1315.50 USD (charge, P(credit)=0.01)
credit : $50.00 -> 50.00 USD (credit, P(credit)=0.99)
candidates : ['$1,200.00', '$115.50', '$1,315.50', '$50.00']
Итоговая сумма составляет $1,315.50, а кредит — $50.00, обе суммы в долларах США (USD). Вопрос Noul о кредите или начислении возвращает 0.01 для общей суммы и 0.99 для кредита, благодаря чему код точно знает знак каждого создаваемого значения Decimal.
Функция
to_decimalпредполагает, что запятая разделяет тысячи, а точка служит десятичным разделителем. Это верно для$1,315.50; в европейской записи€1.315,50все наоборот. Задайте вопросNoulо том, какой формат принят в документе, и выполняйте ветвление в коде.
Открыть в TypeSafe Playground
Ссылка для открытия ветки писем в браузере с вопросом о квитанции и четырьмя адресами кандидатов.
receipt_criteria = {e: None for e in emails} | {
NONE: "None of these is the requested value."
}
playground_link = make_playground_link(
EMAIL_DOC,
{
"receipt": Choice(
instructions="Which email address does the sender want their receipt sent to?",
criteria=receipt_criteria,
)
},
models=[TYPESAFE_MODEL],
)
display(
Markdown(
f"🔗 [Open this thread + selection in the TypeSafe playground]({playground_link})"
)
)
Открыть эту переписку + выбор в TypeSafe Playground →
Два ограничения
- Вопрос
Choiceдопускает не более 255 вариантов ответа. Если кандидатов больше, сужайте поиск в два этапа: сначала выберите раздел, затем конкретный фрагмент внутри него. - Основная работа заключается в нахождении кандидатов. Для email-адресов, номеров телефонов и сумм существуют надежные регулярные выражения; для имен людей готового шаблона нет, поэтому список кандидатов должен формироваться из имеющейся базы, NER-модели (распознавания именованных сущностей) или LLM. После этого TypeSafe выбирает именно того кандидата, который требуется в вопросе.