構造復元
2つのリクエストで書式設定を失ったプレーンテキストからMarkdownを再構築します。1つはハードラップされた行を結合し、もう1つは関連する場合のみ読み込むコンパニオン質問を用いて、各ブロック(見出し、リスト、コード、注釈)を分類します。
本文はenからの機械翻訳です。校正は未実施で、参考情報としてのみご利用ください。

このクックブックは、マークアップが剥ぎ取られたプレーンテキスト(文中でハードラップされ、見出しマーカーやリストの箇条書きがない状態)を取り込み、見出し、段落、リスト、引用、コード、注釈といった Markdown 構造に再構築します。入力となるのは、まさにその状態のチームメモです。
テキスト生成モデルはテキストを Markdown に書き換えることができるが、書き換えは単語を変更することもある。ここではモデルはテキストを生成しない:それはドキュメントに関する狭い質問に答える(この行は途中から始まっているか?このブロックの内容の種類は何か?)、そしてレンダリングはコードが行うため、出力のすべての文字は入力から来ており、すべての判断には確率が伴う。
ドキュメントごとに2回のAPIリクエストを順次実行する、全体のパイプラインは以下の通りです:
- パス1、結合: 隣接する行のペアごとに1つの
Noul質問(「はい」が正しい確率が回答となる、はい/いいえの質問)を1つ用意し、その行の改行が文を2つに分割したかどうかを尋ねます。すべてのペアを1つのリクエストにまとめ、分割された文を引き続き含む行はブロックに再結合されます。 - パス2、分類: 結合されたブロックごとに1つの
Choice質問(選択肢リストから1つを選び、各選択肢に確率を付与)を1つ用意し、見出し、段落、リスト項目、引用、コード、またはコールアウト(本文から区別された注記、ヒント、または警告)の中から分類します。ブロックはパス1の回答が存在して初めて成立するため、これは2回目のリクエストとなります。また、各ブロックにはコンパニオン質問(見出しのレベル、ステップの順序、コールアウトの種類)も付随しており、ブロックのタイプがそれらを必要とする場合にのみ、その回答が参照されます。 - 直接証拠はコード内に留まる。 空白行や明示的なマーカー(
-、1.、#)はコード内で読み取られ、モデルに再考させるために送信されることはありません。このメモは空白行を保持しましたが、すべてのマーカーを失いました。モデルに提供されるのは、テキストからコードでは回答できない質問のみです。
すべての振る舞いは、pass-2 の質問基準で指定されています:1行の説明からなる3つの辞書、および classify_questions 内のステップ質問の真偽基準。残りのコードはそれらを囲む配線です。コストとレイテンシの数は付録にあります:このメモに対して、2往復、10,211トークン、0.8秒、$0.0015 です。
セットアップ
pip install ipython "typesafe-sdk>=0.5.7" cooksafe --extra-index-url https://pypi.typesafe.ai/
その後、TYPESAFE_API_KEYを設定します。すべてのAPI呼び出しはjson_cache.jsonにキャッシュされ、これはクックブックに同梱されているため、再レンダリングしてもAPIを呼び出すことなく公開済みの数値が再生されます。すべての処理をライブで再実行するには、そのファイルを削除してください。
import os
import re
import urllib.request
from pathlib import Path
from time import perf_counter
from cooksafe import JsonCache, make_playground_link
from IPython.display import Markdown, display
from typesafe_sdk import Choice, Noul, NoulCriteria, TypeSafeClient
TYPESAFE_MODEL = "jev-1.12"
PRICE = (0.042, 0.00) # $ per 1M tokens (input, output); TypeSafe jev-1.12 as of 2026-09
client = TypeSafeClient(api_key=os.environ["TYPESAFE_API_KEY"], timeout=120.0)
json_cache = JsonCache(Path("json_cache.json"))
ドキュメント:フォーマットを失ったチームメモ
テスト文書はビルドシステムの移行に関するメモで、プレーンテキストの受信トレイに届いた状態です。文節途中でハードラップされた段落、単独の行に置かれたシェルコマンド、箇条書きや番号のない2つのリスト、何のマークもない警告が含まれています。クックブックの番号が再現可能になるよう、このテキストは固定されたジストから取得されます。
GIST = (
"https://gist.githubusercontent.com/eugene-shvarts/6df7daf97233bf92bcdd6b386a0fa561"
"/raw/5da03690611fb6ddcbaabdb91fb9f91d9751b113/build-memo.txt"
)
@json_cache
def fetch_document(url: str) -> str:
request = urllib.request.Request(url, headers={"User-Agent": "typesafe-cookbook/1.0"})
with urllib.request.urlopen(request) as response:
return response.read().decode()
RAW = fetch_document(GIST)
print(RAW[:560])
Migration to the new build system
Hi everyone, quick heads up about the build system migration that is
happening next week. We have been running the new pipeline in shadow
mode for three weeks and the results look solid, so it is time to
make the switch for real.
What changes for you
The old make targets keep working until the end of the month. The new
entrypoint is a single command that wraps everything, including the
docs build that used to be separate.
bun run build
Generated artifacts no longer need to be committed. The new pipeline
uploads them
行の分割、空白行の追跡、および ID タグ付けはすべてコード内で実行され、モデルは関与しません。
各行には短い ID (L014| ) が付与されます。この ID は通常のテキストであり、モデルは状態の一部としてこれを読み取り、質問と回答はこれらの ID を用いて行を参照します(セマンティック検索クックブックと同じスキーム)。
def to_lines(text: str) -> list[dict]:
lines, gap = [], False
for raw in text.split("\n"):
stripped = re.sub(r"[\t ]+", " ", raw).strip()
if not stripped:
gap = bool(lines) # a leading blank is not a break
continue
lines.append({"text": stripped, "gap": gap})
gap = False
return lines
def tag(items: list[dict], prefix: str) -> str:
return "\n".join(
f"{chr(10) if item['gap'] else ''}{prefix}{i:03d}| {item['text']}"
for i, item in enumerate(items)
)
def line_id(i: int) -> str:
return f"L{i:03d}"
def block_id(i: int) -> str:
return f"B{i:03d}"
LINES = to_lines(RAW)
print(f"{len(LINES)} non-blank lines. The model sees, e.g.:")
print("\n".join(tag(LINES, "L").splitlines()[19:24]))
28 non-blank lines. The model sees, e.g.:
L013| The cutover touches three teams, so check whether you are on this
L014| list before you plan anything for Monday:
L015| The platform team
L016| The web client team
L017| Whoever still owns the release tooling
パス1:分割された文の結合
隣接する2行ごとに1つの質問を、1つのリクエストで全て行います;空白行で区切られたペアはスキップされます。質問は意図的に狭い範囲に限定されており(「この行は途中の文を引き継いでいるか?」)、これはテキストに関する客観的事実に近いです。付録では、表現の選択とマージ閾値の導出方法の両方を扱っています。
def join_question(i: int) -> Noul:
return Noul(
instructions=f"Does line {line_id(i)} pick up mid-sentence, continuing a sentence left unfinished at the end of line {line_id(i - 1)}?",
criteria=NoulCriteria(
true="The line starts in the middle of a sentence that began on the previous line - the line break tore the sentence apart",
false="The line begins a new sentence, item, heading, or thought of its own",
),
)
@json_cache
def stitch(wording: str = "mid-sentence") -> dict:
make = join_question if wording == "mid-sentence" else naive_join_question
questions = {line_id(i): make(i) for i in range(1, len(LINES)) if not LINES[i]["gap"]}
started = perf_counter()
response = client.system_one(
state=tag(LINES, "L"), questions=questions, model=TYPESAFE_MODEL
)
return {
"joins": [
response.answers[line_id(i)].noul if line_id(i) in response.answers else 0.0
for i in range(len(LINES))
],
"seconds": round(perf_counter() - started, 2),
"usage": [response.usage.input_tokens, response.usage.output_tokens],
}
result = stitch()
print(f"{sum(1 for l in LINES if not l['gap']) - 1} pair questions, one request, "
f"{result['seconds']}s")
16 pair questions, one request, 0.32s
マージの cutoff は、前の行の終わりの仕方によって異なります。句読点で終わらない行(dangling line)の後は、結合確率が 0.2 以上であればペアがマージされます;終端の句読点(. ! ? : ;)の後は、cutoff が 0.5 に上昇します。付録では、これらの2つの数値の背後にある確率について詳しく解説しています。
JOIN_AFTER_DANGLING, JOIN_AFTER_TERMINAL = 0.2, 0.5
def ends_terminal(text: str) -> bool:
return re.search(r'[.!?:;…]["\')\]]*$', text) is not None
def merge(joins: list[float]) -> list[dict]:
blocks = []
for i, line in enumerate(LINES):
bar = (
JOIN_AFTER_TERMINAL
if i and ends_terminal(LINES[i - 1]["text"])
else JOIN_AFTER_DANGLING
)
if blocks and not line["gap"] and joins[i] >= bar:
blocks[-1]["text"] += " " + line["text"]
blocks[-1]["lines"].append(i)
else:
blocks.append({"text": line["text"], "lines": [i], "gap": line["gap"]})
return blocks
blocks = merge(result["joins"])
healed = len(LINES) - len(blocks)
print(f"{len(LINES)} lines -> {len(blocks)} blocks ({healed} line breaks healed)")
for i, block in enumerate(blocks):
n = len(block["lines"])
print(f"{block_id(i)} {n} line{'s' if n > 1 else ' '} {block['text'][:62]}")
28 lines -> 17 blocks (11 line breaks healed)
B000 1 line Migration to the new build system
B001 4 lines Hi everyone, quick heads up about the build system migration t
B002 1 line What changes for you
B003 3 lines The old make targets keep working until the end of the month.
B004 1 line bun run build
B005 3 lines Generated artifacts no longer need to be committed. The new pi
B006 2 lines The cutover touches three teams, so check whether you are on t
B007 1 line The platform team
B008 1 line The web client team
B009 1 line Whoever still owns the release tooling
B010 1 line Things to do before Monday
B011 1 line Update your local toolchain to version 2.4 or later
B012 1 line Delete the old build cache directory
B013 1 line Run the doctor script and fix anything it flags
B014 3 lines If the doctor script reports a red result on the toolchain che
B015 2 lines As Dana put it in the kickoff, "a migration nobody notices is
B016 1 line Thanks, and shout if anything looks off.
パス 2: ブロックの分類
各 stitching ブロックには Choice の質問が一つ付きます:これはどのような種類の内容ですか? これら3つの辞書と、classify_questions 以下のステップ質問の真偽基準は、分類器の仕様全体です。他のロジックはありません。パイプラインをあなた自身のドキュメントに適応させるには、これらの説明を編集してください。
TYPE_CRITERIA = {
"heading": "A short label or title that names the document or the section that follows it - not a full sentence of content",
"paragraph": "Running prose: one or more complete sentences of explanatory or narrative text",
"list_item": "One entry in a list of parallel items - an ingredient, a feature, a task, an attendee; reads as one of several sibling entries",
"quote": "Words attributed to a person or source - quoted speech, a citation, an excerpt someone else wrote",
"code": "Computer code, a shell command, terminal output, or a config snippet meant to be read verbatim",
"callout": "A warning, tip, or important note that interrupts the flow to flag something the reader must not miss",
}
HLEVEL_CRITERIA = {
"title": "The title of the whole document",
"section": "A major section heading within the document",
"subsection": "A minor heading nested under a section",
}
CALLOUT_CRITERIA = {
"note": "Neutral extra information the reader should be aware of",
"tip": "A helpful suggestion or shortcut that makes things easier",
"warning": "A caution about something that can go wrong or cause harm",
}
以下すべては配管作業です:質問を構築し、1つのリクエストを送信し、回答を読み取ります。
型が heading で返ってきた場合、レンダラーは見出しレベルを必要とします;list_item の場合、順序が重要かどうか;callout の場合、どの種類か。型はまだ不明であり、それらを待つことは3回目のラウンドトリップを意味するため、補完質問は同じリクエスト内で事前に尋ねられます。これらの回答の多くは決して読み取られません:段落のステップ確率は意味を成さず、単に無視されます。追加の質問はほとんど追加されません。なぜなら、状態はほぼすべてのトークンを含み、いずれにせよ1回だけ送信されるのに対し、追加のラウンドトリップは1つのリクエスト分のレイテンシを追加するからです。
HEADING_MAX_CHARS = 90 # longer blocks can't render as headings, so don't ask
def classify_questions(texts: list[str]) -> dict:
questions = {}
for i, text in enumerate(texts):
bid = block_id(i)
questions[f"type_{bid}"] = Choice(
instructions=f"What kind of content is block {bid}?", criteria=TYPE_CRITERIA
)
if len(text) <= HEADING_MAX_CHARS:
questions[f"hlevel_{bid}"] = Choice(
instructions=f"As a heading, what level would block {bid} occupy in this document's structure?",
criteria=HLEVEL_CRITERIA,
)
questions[f"step_{bid}"] = Noul(
instructions=f"Is block {bid} an instruction in a sequence where the order of the items matters?",
criteria=NoulCriteria(
true="It is one step of a procedure - the items around it must happen in order",
false="Order is irrelevant - it is a loose collection, or not a list item at all",
),
)
questions[f"callout_{bid}"] = Choice(
instructions=f"What kind of aside is block {bid}?", criteria=CALLOUT_CRITERIA
)
return questions
@json_cache
def classify(texts: list[str], gaps: list[bool]) -> dict:
tagged = tag([{"text": t, "gap": g} for t, g in zip(texts, gaps)], "B")
questions = classify_questions(texts)
started = perf_counter()
response = client.system_one(state=tagged, questions=questions, model=TYPESAFE_MODEL)
judgments = []
for i in range(len(texts)):
bid = block_id(i)
type_answer = response.answers[f"type_{bid}"]
hlevel = response.answers.get(f"hlevel_{bid}")
judgments.append(
{
"type": type_answer.choice,
"confidence": type_answer.confidence,
"probabilities": type_answer.probabilities,
"hlevel": hlevel.choice if hlevel else "section",
"step": response.answers[f"step_{bid}"].noul,
"callout": response.answers[f"callout_{bid}"].choice,
}
)
return {
"judgments": judgments,
"n_questions": len(questions),
"seconds": round(perf_counter() - started, 2),
"usage": [response.usage.input_tokens, response.usage.output_tokens],
}
classified = classify([b["text"] for b in blocks], [b["gap"] for b in blocks])
for block, judgment in zip(blocks, classified["judgments"]):
block.update(judgment)
print(f"{classified['n_questions']} questions about {len(blocks)} blocks, one request, "
f"{classified['seconds']}s\n")
print(f"{'block':<6}{'type':<11}{'conf':<6}{'companion used':<18}text")
for i, b in enumerate(blocks):
companion = {
"heading": f"level={b['hlevel']}",
"list_item": f"step={b['step']:.2f}",
"callout": f"kind={b['callout']}",
}.get(b["type"], "-")
print(f"{block_id(i):<6}{b['type']:<11}{b['confidence']:.2f} {companion:<18}"
f"{b['text'][:46]}")
62 questions about 17 blocks, one request, 0.51s
block type conf companion used text
B000 heading 0.99 level=title Migration to the new build system
B001 paragraph 0.98 - Hi everyone, quick heads up about the build sy
B002 heading 0.75 level=section What changes for you
B003 paragraph 0.89 - The old make targets keep working until the en
B004 code 1.00 - bun run build
B005 paragraph 0.90 - Generated artifacts no longer need to be commi
B006 paragraph 0.43 - The cutover touches three teams, so check whet
B007 list_item 0.99 step=0.15 The platform team
B008 list_item 1.00 step=0.16 The web client team
B009 list_item 0.99 step=0.12 Whoever still owns the release tooling
B010 heading 0.96 level=section Things to do before Monday
B011 list_item 0.98 step=0.86 Update your local toolchain to version 2.4 or
B012 list_item 0.99 step=0.87 Delete the old build cache directory
B013 list_item 0.92 step=0.90 Run the doctor script and fix anything it flag
B014 callout 0.65 kind=warning If the doctor script reports a red result on t
B015 quote 0.99 - As Dana put it in the kickoff, "a migration no
B016 paragraph 0.92 - Thanks, and shout if anything looks off.
すべてのブロックの判断は上記の表にあり、補完カラムには事前回答がどのように活用されているかが示されています:「月曜までにやるべきこと」の3行はステップ確率が約0.9で(番号付きリストとしてレンダリングされます)、3つのチーム行は約0.1(箇条書き)であり、医師スクリプトに関する無印の警告はwarning種のカールアウトとして分類されました。付録では、モデルが確信を持てなかった1つのブロックについて考察します。
レンダリング
コードは、判断結果からページを組み立てる。連続するリスト項目は1つのリストになり、項目のステップ確率の平均が0.5以上の場合に番号が振られる。この閾値は、個別の質問を直接行うことではなく、グループレベルでの判断である。
STEP_THRESHOLD = 0.5
HEADING_MARK = {"title": "#", "section": "##", "subsection": "###"}
CALLOUT_MARK = {"note": "NOTE", "tip": "TIP", "warning": "WARNING"}
def to_markdown(blocks: list[dict]) -> str:
groups = []
for b in blocks:
if b["type"] in ("list_item", "code") and groups and groups[-1][0] == b["type"]:
groups[-1][1].append(b)
else:
groups.append((b["type"], [b]))
parts = []
for kind, items in groups:
if kind == "list_item":
ordered = sum(b["step"] for b in items) / len(items) >= STEP_THRESHOLD
parts.append("\n".join(
f"{n + 1}. {b['text']}" if ordered else f"- {b['text']}"
for n, b in enumerate(items)
))
elif kind == "code":
parts.append("```\n" + "\n".join(b["text"] for b in items) + "\n```")
elif kind == "heading":
parts.append(f"{HEADING_MARK[items[0]['hlevel']]} {items[0]['text']}")
elif kind == "quote":
parts.append(f"> {items[0]['text']}")
elif kind == "callout":
parts.append(f"> [!{CALLOUT_MARK[items[0]['callout']]}]\n> {items[0]['text']}")
else:
parts.append(items[0]["text"])
return "\n\n".join(parts) + "\n"
markdown = to_markdown(blocks)
print(markdown)
# Migration to the new build system
Hi everyone, quick heads up about the build system migration that is happening next week. We have been running the new pipeline in shadow mode for three weeks and the results look solid, so it is time to make the switch for real.
## What changes for you
The old make targets keep working until the end of the month. The new entrypoint is a single command that wraps everything, including the docs build that used to be separate.
```
bun run build
```
Generated artifacts no longer need to be committed. The new pipeline uploads them to the registry automatically, and checking them in just creates merge conflicts.
The cutover touches three teams, so check whether you are on this list before you plan anything for Monday:
- The platform team
- The web client team
- Whoever still owns the release tooling
## Things to do before Monday
1. Update your local toolchain to version 2.4 or later
2. Delete the old build cache directory
3. Run the doctor script and fix anything it flags
> [!WARNING]
> If the doctor script reports a red result on the toolchain check, do not proceed with the migration. Ping the infra channel first and we will sort it out together.
> As Dana put it in the kickoff, "a migration nobody notices is the only kind worth shipping."
Thanks, and shout if anything looks off.
上記のすべての単語は入力からのものである。パイプラインは境界、タイプ、 マークアップを選択しただけである。
プレイグラウンドで開く
この共有リンクには、結合されたブロックと完全な Pass-2 質問セットが含まれています。これを開いて分類をライブで再実行してください。
playground_link = make_playground_link(
tag(blocks, "B"),
classify_questions([b["text"] for b in blocks]),
models=[TYPESAFE_MODEL],
)
display(Markdown(f"🔗 [Open the stitched memo + questions in the TypeSafe playground]({playground_link})"))
TypeSafeプレイグラウンドでステッチされたメモと質問を開く →
付録
コストとレイテンシ
tokens = [result["usage"], classified["usage"]]
total_in, total_out = sum(t[0] for t in tokens), sum(t[1] for t in tokens)
cost = total_in / 1e6 * PRICE[0] + total_out / 1e6 * PRICE[1]
n_joins = sum(1 for l in LINES if not l["gap"]) - 1
print(f"pass 1 {n_joins} questions {result['seconds']}s")
print(f"pass 2 {classified['n_questions']} questions {classified['seconds']}s")
print(f"total {total_in + total_out:,} tokens "
f"{result['seconds'] + classified['seconds']:.1f}s ${cost:.4f}")
pass 1 16 questions 0.32s
pass 2 62 questions 0.51s
total 10,211 tokens 0.8s $0.0003
2往復、10,211トークン、0.8秒、$0.0015。
ジoin閾値の由来
パス1からの行ごとの結合確率:
print("join line")
for i, line in enumerate(LINES[:18]):
join = " " if i == 0 or line["gap"] else f"{result['joins'][i]:.2f}"
print(f"{join} {line_id(i)}| {line['text'][:66]}")
join line
L000| Migration to the new build system
L001| Hi everyone, quick heads up about the build system migration that
0.77 L002| happening next week. We have been running the new pipeline in shad
0.62 L003| mode for three weeks and the results look solid, so it is time to
0.39 L004| make the switch for real.
L005| What changes for you
L006| The old make targets keep working until the end of the month. The
0.42 L007| entrypoint is a single command that wraps everything, including th
0.59 L008| docs build that used to be separate.
L009| bun run build
L010| Generated artifacts no longer need to be committed. The new pipeli
0.48 L011| uploads them to the registry automatically, and checking them in
0.40 L012| just creates merge conflicts.
L013| The cutover touches three teams, so check whether you are on this
0.50 L014| list before you plan anything for Monday:
0.22 L015| The platform team
0.11 L016| The web client team
0.12 L017| Whoever still owns the release tooling
確率は2つの明確な帯に分かれる:文を分割する改行は0.39以上のスコアを付け、著者が意図した改行は0に近いスコアになる。ただし、この2つの帯の間にどこで閾値を設けるかは、直前の行がどのように終わるかに依存し、これはコードから直接読み取ることができる事実である:
- 文脈に孤立した行(文の終わりを示す句読点がない行)の直後では、0.2以上の値はすべて継続と見なされる。ここでは真の継続でも最低0.39のスコアになる(
L004| make the switch for real.)ため、0.5という単一の慎重なカットオフ値を設定すると、健全な段落が分断されてしまう。 - 終止句読点(文や節の終わりを示す文字:
.!?:;)の直後では、カットオフ値は0.5に引き上げられる。このメモのチームリストを見るとその理由がわかる:L015| The platform teamはコロンに続き、スコアは0.22である。これは低いがゼロではない「文が続いている」という信号であり、0.2のカットオフ値をクリアして、リストを導入している文と結合されてしまう。単一の閾値では両方のケースに対応できない。コードがまず句読点をチェックすれば、この2つの範囲は明確に分離される。
なぜこの質問が「文中」であり「同じ段落」ではないのか
このパイプラインの最初のバージョンは、自明な問いを投げかけた。「これらの2行は同じ段落の一部なのか?」それは特定の方法で失敗した。見出しの下にある短い行の連続(箇条書きなしで入力されたリスト)は、緩やかな意味で段落である:行はまとまっており、共通の主題を共有している。段落について問われると、モデルはすべてのペアに対して「はい」と答え、結合処理はリスト全体を1つの長いブロックに統合してしまう。
同じドキュメント、同じリクエスト形状、変更されたのは文言のみ:
def naive_join_question(i: int) -> Noul:
return Noul(
instructions=f"Are lines {line_id(i - 1)} and {line_id(i)} part of the same paragraph?",
criteria=NoulCriteria(
true="The two lines belong to the same paragraph of running text",
false="The two lines belong to different paragraphs or different pieces of content",
),
)
naive = stitch("same-paragraph")
print(f"{'':14}{'mid-sentence':>13}{'same paragraph':>16}")
for i in (15, 16, 17, 20, 21):
print(f"{line_id(i)}{'':2}{LINES[i]['text'][:36]:<38}"
f"{result['joins'][i]:>7.2f}{naive['joins'][i]:>13.2f}")
print(f"\nblocks after merge: {len(blocks)} (mid-sentence) vs "
f"{len(merge(naive['joins']))} (same paragraph)")
mid-sentence same paragraph
L015 The platform team 0.22 0.77
L016 The web client team 0.11 0.81
L017 Whoever still owns the release tooli 0.12 0.78
L020 Delete the old build cache directory 0.08 0.88
L021 Run the doctor script and fix anythi 0.05 0.91
blocks after merge: 17 (mid-sentence) vs 12 (same paragraph)
段落の wording について、マークされていないリスト項目はすべて 0.75 以上のスコアを獲得し、両方のリストが収束する。メモは数個のつなぎ合わせられたブロックに統合される。「同じ段落」は、トピックが継続しているかどうかをモデルに判断させるもので、リスト項目の間ではそれが当てはまる。「文中で途中から続く」は、テキストそのものについて問うものである。判断が閾値にフィードバックされる場合、質問はそれを決定する最も狭い事実を名指しすべきである。ここで wording がもたらす違いは、17 ブロックと 12 ブロックとの差である。
最も信頼度が低いブロック
uncertain = min(blocks, key=lambda b: b["confidence"])
print(f'"{uncertain["text"]}"')
print(f"confidence {uncertain['confidence']:.2f}: ", end="")
print(", ".join(f"{k} {v:.2f}" for k, v in
sorted(uncertain["probabilities"].items(), key=lambda kv: -kv[1])[:3]))
"The cutover touches three teams, so check whether you are on this list before you plan anything for Monday:"
confidence 0.43: paragraph 0.53, list_item 0.24, callout 0.19
チーム一覧を紹介する文は本質的に曖昧である — それは続くもの(見出し風)を名指しし、完全な文(段落風)であり、コールアウトが配置されるべき場所に位置している。確率はそれに応じて分散する(段落 0.53、リスト項目 0.24、コールアウト 0.19)、そして UI はそれを提示できる — 例えば、勝利選択肢の背後にある確率(タイプ信頼度)が 0.55 未満であるブロックのいずれかに下線を引いてレビュー対象として浮上させる。