ДокументацияПрактические руководства (Cookbooks)Извлечение предварительно разобранных значений

Извлечение предварительно разобранных значений

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 делает выбор, а код забирает результат в три шага:

  1. Регулярное выражение находит в тексте потенциальные значения. Настройте его с упором на полноту (recall), чтобы находить с запасом.
  2. TypeSafe выбирает, какого именно кандидата требует вопрос, и считывает любые атрибуты, необходимые коду далее (валюта, страна, является ли сумма начислением или списанием).
  3. Программный код копирует выбранное значение и нормализует его.

Поскольку TypeSafe всегда выбирает строго среди фрагментов, найденных регулярным выражением, возвращаемое значение является одним из этих исходных фрагментов без каких-либо изменений. Модель не способна выдумать значение или случайно переставить цифры местами.

Overview diagram

Регулярное выражение находит потенциальные значения в документе, TypeSafe выбирает нужное, а последующий код нормализует его и выполняет целевое действие.

Настройка

BASH THEME={NULL} api.wedstack.ru/v1
pip install ipython phonenumbers "typesafe-sdk>=0.5.7" cooksafe --extra-index-url https://pypi.typesafe.ai/

Затем установите переменную TYPESAFE_API_KEY.

PYTHON THEME={NULL} api.wedstack.ru/v1
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.

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1

Электронная почта: выбор правильного адреса по роли

В заголовках письма четыре адреса. В тексте отправитель просит выслать квитанцию на личный адрес вместо адреса биллинга из поля To:, поэтому правильный ответ зависит от понимания контекста письма. Задаем два вопроса: на какой адрес отправить квитанцию и с какого адреса отправлено сообщение.

PYTHON THEME={NULL} api.wedstack.ru/v1
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})")
PLAINTEXT api.wedstack.ru/v1
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, начинающийся с + и кода страны.

PYTHON THEME={NULL} api.wedstack.ru/v1
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}")
PLAINTEXT api.wedstack.ru/v1
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.

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1
PLAINTEXT api.wedstack.ru/v1
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

Ссылка для открытия ветки писем в браузере с вопросом о квитанции и четырьмя адресами кандидатов.

PYTHON THEME={NULL} api.wedstack.ru/v1
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 выбирает именно того кандидата, который требуется в вопросе.