사전 파싱 값 추출
정규식으로 후보 이메일, 전화번호, 금액을 찾은 뒤, TypeSafe가 요청된 스팬을 선택하게 하여 코드가 원문 그대로의 값을 정규화할 수 있게 합니다.
정규식이 후보 값을 찾고, TypeSafe가 질문이 요구하는 값을 고르면, 코드가 그것을 원문 그대로 복사합니다.
여기서의 find와 pick 조합은 여러분의 문서에도 적용할 수 있으며, 세 가지 예제가 사용법을 보여줍니다. 발신자가 영수증을 받고 싶어 하는 주소, +14155550177 형태의 전화번호, 그리고 청구(charge)로 표시된 1315.50 USD 금액의 인보이스 합계입니다.
TypeSafe는 여러분이 넘긴 선택지 중 하나를 고르므로, 후보를 먼저 찾아야 합니다. 정규식이 후보를 찾고, TypeSafe가 하나를 고르고, 코드가 그 선택을 복사하는 세 단계입니다.
- 정규식이 텍스트에서 후보 값을 찾습니다. 과다 검출되도록 조정하십시오.
- TypeSafe가 질문이 요구하는 후보가 무엇인지 고르고, 코드가 이후에 필요로 하는 속성(통화, 국가, 금액이 입금인지 청구인지)을 읽어냅니다.
- 코드가 선택된 값을 복사하고 정규화합니다.
TypeSafe는 정규식이 찾은 스팬 중에서만 고르므로, 돌려받는 값은 그 스팬 중 하나를 그대로 복사한 것입니다. 값을 지어내거나 숫자를 뒤바꿀 수 없습니다.
정규식이 문서에서 후보 값을 찾고, TypeSafe가 하나를 고르면, 이후 코드가 그것을 정규화하여 처리합니다.
준비
pip install ipython phonenumbers 'cooksafe>=0.2.0,<0.3.0'
그다음 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은 선택지가 find가 반환한 스팬들인 Choice 질문이므로, 그 답은 그 스팬 중 하나를 정확히 복사한 것이거나, 맞는 후보가 없을 때는 none입니다. classify는 고정된 레이블 집합에 대한 Choice 질문으로, 여기서는 통화와 국가에 사용합니다. is_true는 Noul이며, 여기서는 금액이 입금(credit)인지 묻는 데 사용합니다.
모든 호출은 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는 본문이 요청한 대로 Reply-To: 줄에 있는 개인 Gmail 주소이고, 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가 청구 총액과 입금(credit)을 고르고, 통화를 읽으며, 고른 각 금액을 청구(charge) 또는 입금(credit)으로 표시합니다. 코드는 고른 각 문자열을 복사해 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개까지 허용합니다. 후보가 그보다 많으면 두 단계로 좁히십시오. 먼저 절을 고르고, 그 안에서 스팬을 고릅니다.- 후보를 찾는 것이 일이 많이 드는 부분입니다. 이메일, 전화번호, 금액에는 그것을 포괄하는 정규식이 있습니다. 이름에는 없으므로, 그 후보는 이미 가지고 있는 명부나, 개체명 인식기, 혹은 후보를 제안하는 LLM에서 나와야 합니다. 그런 다음 TypeSafe가 질문이 요구하는 하나를 고릅니다.