Pesquisa linha a linha
Construa pesquisa semântica para os Termos de Serviço do GitHub. Em uma única solicitação, pontue 218 line ids contra uma consulta em linguagem simples com uma Choice question, e use uma Noul question para verificar se o documento contém uma resposta.
Traduzido automaticamente do en, sem revisão. Apenas como referência rápida.

Você tem os Termos de Serviço do GitHub e uma pergunta em linguagem simples sobre eles. Você precisa das linhas que respondem à pergunta e de uma maneira de detectar quando o documento não tem resposta. As consultas incluídas classificam as linhas com respostas diretas primeiro. Os limiares exists classificam os casos restantes como ausentes ou parciais. Você acaba com find(), que retorna a probabilidade exists e uma pontuação de relevância por linha.

O backend de busca é composto por três partes:
- Tag each line with an ID so TypeSafe can point to it.
- Use a
Choicequestion to rank those line IDs by how well they answer the query. Choice question probabilities always add up to 1, so a line ranks first even when none answer the query. - In the same request, use a
Noulquestion to check whether the document contains an answer at all.
Configuração
Obter uma chave de API TypeSafe
Crie uma chave no console do TypeSafe e exporte-a:
export TYPESAFE_API_KEY="your-key-here"
Instale as dependências
pip install "typesafe-sdk>=0.5.7" cooksafe \
--extra-index-url https://pypi.typesafe.ai/
JsonCache reproduz as respostas da API incluídas, para que as etapas abaixo sejam executadas sem uma chave de API ou qualquer gasto. Para tornar as solicitações reais, defina TYPESAFE_API_KEY e exclua json_cache.json.
Criar o script
Comece semantic_search.py com as importações e o cliente:
import os
import urllib.request
from pathlib import Path
from cooksafe import JsonCache
from typesafe_sdk import Choice, Noul, NoulCriteria, TypeSafeClient
TYPESAFE_MODEL = "jev-1.12"
client = TypeSafeClient(
api_key=os.environ.get("TYPESAFE_API_KEY", "cache-only"), timeout=120.0
)
json_cache = JsonCache(Path("json_cache.json"))
Passo 1: etiquete cada linha com um ID
O documento de teste é os Termos de Serviço do GitHub, divididos em 218 cláusulas, então cada resultado de pesquisa aponta para uma linha citável.
Adicionar ao semantic_search.py:
GIST = (
"https://gist.githubusercontent.com/eugene-shvarts/900632789a24983d5678ffd508dd01f6"
"/raw/cf9c2ab422d568deade949ef0a06bed6896964b9/github-tos.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()
LINES = fetch_document(GIST).splitlines()
O cache impede downloads repetidos, e splitlines() deixa uma lista de 218 strings.
Agora, adicione um ID curto a cada linha e junte as linhas de volta em um único documento. O modelo usa esses IDs para apontar para sua resposta.
def line_id(i: int) -> str:
return f"L{i:03d}"
DOCUMENT = "\n".join(f"{line_id(i)}| {line}" for i, line in enumerate(LINES))
DOCUMENT agora parece assim:
L052| You own Your Content. If you post Content you did not create, you are responsible for...
L053| You grant us and other Users the licenses in Sections D.4–D.8. These licenses apply...
L054| 4. License Grant to Us
Passo 2: pergunte onde está a resposta
Uma Choice pergunta retorna uma probabilidade para cada opção. Use os IDs de linha como as
opções,
e “escolha uma opção” torna-se “aponte para uma linha.”
def where_question(query: str) -> Choice:
return Choice(
instructions=f'Which line of the document contains the answer to: "{query}"?',
criteria={line_id(i): None for i in range(len(LINES))},
)
As descrições das opções são None porque o documento já contém o texto para cada ID. A consulta vai em instructions; o estado permanece inalterado entre as pesquisas.
Nota — Uma pergunta
Choiceaceita até 255 opções, portanto esta receita pesquisa documentos de até 255 linhas em uma única solicitação. Além disso, pesquise em duas etapas: uma pergunta Choice seleciona uma janela de linhas, e uma segunda classifica as linhas dentro dela.
Passo 3: verificar se existe uma resposta
As probabilidades de escolha sempre somam 1, então algumas linhas ficam em primeiro lugar mesmo quando o documento não responde à pergunta. A classificação por si só não consegue distinguir uma resposta real da linha irrelevante mais próxima.
Então faça uma segunda pergunta, no mesmo pedido:
def exists_question(query: str) -> Noul:
return Noul(
instructions=f'Does any line of the document address or answer: "{query}"?',
criteria=NoulCriteria(
true="At least one line of the document states or directly implies the answer",
false="No line of the document addresses this",
),
)
Ao contrário das probabilidades de Escolha, a probabilidade Noul não depende das outras opções, por isso pode cair perto de zero quando o documento não tem resposta.
Passo 4: envie ambas as perguntas em uma única solicitação
O método system_one responde a ambas as perguntas em uma única passagem. O estado é enviado uma vez, portanto, adicionar a verificação de existência requer apenas uma pequena quantidade de saída extra.

@json_cache
def _find(
model: str,
state: str,
where: Choice,
exists: Noul,
) -> dict:
response = client.system_one(
state=state,
questions={"where": where, "exists": exists},
model=model,
)
probabilities = response.answers["where"].probabilities
return {
"exists": response.answers["exists"].noul,
"relevance": [probabilities.get(line_id(i), 0.0) for i in range(len(LINES))],
}
def find(query: str) -> dict:
return _find(
TYPESAFE_MODEL,
DOCUMENT,
where_question(query),
exists_question(query),
)
A lista relevance mantém uma pontuação por linha, na ordem do documento.
Passo 5: leia o resultado
Dois trechos de código local concluem a tarefa: verdict() converte a probabilidade bruta de exists em três estados, com um estado intermediário para respostas parciais, e show() renderiza relevance como um gráfico de barras para que a classificação seja legível em um terminal.
FOUND, ABSENT = 0.7, 0.35 # present answers typically read >=0.9, absent <=0.05
def verdict(exists: float) -> str:
if exists >= FOUND:
return "answered in this document"
return "not in this document" if exists < ABSENT else "partially addressed"
def show(query: str, top: int = 4) -> dict:
result = find(query)
print(f'"{query}"')
print(f" exists {result['exists']:.2f} -> {verdict(result['exists'])}")
ranked = sorted(
range(len(LINES)), key=lambda i: result["relevance"][i], reverse=True
)
for i in ranked[:top]:
bar = "#" * max(1, round(result["relevance"][i] * 12))
preview = LINES[i][:58].rstrip()
print(f" {line_id(i)} {result['relevance'][i]:.2f} {bar:<12} {preview}")
return result
Esses limiares separam os exemplos abaixo, mas ajuste-os em relação aos seus próprios documentos antes de usá-los em produção.
Passo 6: execute a pesquisa
Faça duas perguntas que tenham respostas diretas, uma que não tenha resposta e uma que tenha uma resposta parcial, quatro no total.
print(f"{len(LINES)} lines, {len(DOCUMENT):,} characters\n")
show("who owns the code I upload?")
print()
show("can GitHub kick me off the platform without warning?")
print()
show("do I have to take disputes to arbitration?", top=2)
print()
show("can minors use GitHub with parental permission?", top=2)
218 lines, 43,980 characters
"who owns the code I upload?"
exists 0.98 -> answered in this document
L052 0.95 ########### You own Your Content. If you post Content you did not crea
L046 0.02 # Short version: You own content you create, but you allow u
L051 0.02 # 3. Ownership and License Grants
L217 0.01 # Questions about the Terms of Service? Contact us through t
"can GitHub kick me off the platform without warning?"
exists 0.97 -> answered in this document
L168 0.97 ############ GitHub has the right to suspend or terminate your access t
L167 0.03 # 3. GitHub May Terminate
L000 0.00 # Effective date: April 27, 2026 · A. Definitions
L001 0.00 # Short version: We use these basic terms throughout the agr
"do I have to take disputes to arbitration?"
exists 0.14 -> not in this document
L205 0.86 ########## Except to the extent applicable law provides otherwise, th
L168 0.02 # GitHub has the right to suspend or terminate your access t
"can minors use GitHub with parental permission?"
exists 0.46 -> partially addressed
L029 0.90 ########### You must be age 13 or older. While we are thrilled to see
L012 0.07 # “User,” “You,” and “Your” refer to the individual person,
O que as pontuações significam
Os dois primeiros retornam respostas diretas e as linhas da fonte necessárias para verificá-las.
Os outros dois mostram por que a verificação de existência é importante:
- Arbitragem: A classificação dá à linha mais próxima uma pontuação de 0.86, mas
existsé apenas 0.14. A resposta não está no documento. - Permissão dos pais: A regra de idade ocupa o primeiro lugar, mas não responde se a permissão dos pais altera a regra. O resultado é parcialmente abordado.
A classificação indica onde procurar; a pontuação exists indica se o resultado responde à pergunta.
Experimente no seu próprio documento
Abra o contrato marcado no playground do TypeSafe para editar as perguntas
contra o mesmo texto. Para pesquisar o seu próprio, substitua a URL em fetch_document(); cada outra
linha do script funciona com base em LINES.