Busca linha por linha
Crie busca semântica para os Termos de Serviço do GitHub. Em uma única requisição, pontue 218 ids de linha contra uma consulta em linguagem natural com uma pergunta Choice, e use uma pergunta Noul para verificar se o documento contém uma resposta.
Você tem os Termos de Serviço do GitHub e uma pergunta em linguagem natural sobre
eles. Você precisa das linhas que respondem à pergunta e de uma forma de detectar
quando o documento não tem resposta. As consultas incluídas classificam primeiro as
linhas com respostas diretas. Os limiares de exists classificam os casos restantes
como ausentes ou parciais. No fim você tem find(), que retorna a probabilidade de
exists e um score de relevância por linha.
O backend de busca se monta em três partes:
- Marque cada linha com um ID para que o TypeSafe possa apontar para ela.
- Use uma pergunta
Choicepara ordenar esses IDs de linha conforme quão bem eles respondem à consulta. As probabilidades de uma pergunta Choice sempre somam 1, então uma linha fica em primeiro lugar mesmo quando nenhuma responde à consulta. - Na mesma requisição, use uma pergunta
Noulpara verificar se o documento contém alguma resposta.
Configuração
Obtenha uma chave de API do TypeSafe
Crie uma chave no console do TypeSafe e exporte-a:
export TYPESAFE_API_KEY="your-key-here"
Instale as dependências
pip install 'cooksafe>=0.2.0,<0.3.0'
JsonCache reproduz as respostas de API incluídas, então os passos abaixo rodam sem
chave de API e sem gasto. Para deixar as requisições ao vivo, defina TYPESAFE_API_KEY
e apague json_cache.json.
Crie o script
Inicie semantic_search.py com os imports e o client:
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: marque cada linha com um ID
O documento de teste são os Termos de Serviço do GitHub, divididos em 218 cláusulas, então cada resultado de busca aponta para uma linha citável.
Adicione a 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 evita downloads repetidos, e splitlines() deixa uma lista de 218 strings.
Agora prefixe cada linha com um ID curto e junte as linhas de volta em um único documento. O modelo usa esses IDs para apontar para a 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))
Agora DOCUMENT fica 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 pergunta Choice retorna uma probabilidade para cada opção. Use os IDs de linha
como as opções, e “escolha uma opção” vira “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 de cada ID.
A consulta vai em instructions; o estado fica inalterado entre as buscas.
Passo 3: verifique se existe uma resposta
As probabilidades de Choice sempre somam 1, então alguma linha fica em primeiro lugar mesmo quando o documento não responde à pergunta. Só a ordenação não consegue distinguir uma resposta real da linha irrelevante mais próxima.
Então faça uma segunda pergunta, na mesma requisição:
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",
),
)
Diferente das probabilidades de Choice, a probabilidade de Noul não depende das outras opções, então ela pode cair perto de zero quando o documento não tem resposta.
Passo 4: envie as duas perguntas em uma única requisição
O método system_one responde às duas perguntas em uma única passagem. O estado é
enviado uma vez, então adicionar a verificação de existência exige apenas um pouco 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 guarda um score por linha, na ordem do documento.
Passo 5: leia o resultado
Dois trechos de código local terminam o trabalho: verdict() transforma 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 ordenação seja legível no 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 com os seus próprios documentos antes de usá-los em produção.
Passo 6: execute a busca
Faça duas perguntas que têm respostas diretas, uma que não tem resposta e uma que tem 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 os scores significam
As duas primeiras consultas retornam respostas diretas e as linhas de origem necessárias para verificá-las.
As outras duas mostram por que a verificação de existência importa:
- Arbitragem: A ordenação dá à linha mais próxima um score de 0.86, mas
existsé apenas 0.14. A resposta não está no documento. - Permissão dos pais: A regra de idade fica em primeiro lugar, mas ela não responde se a permissão dos pais muda a regra. O resultado é parcialmente abordado.
A ordenação diz onde procurar; o score de exists diz se o resultado responde à
pergunta.
Experimente com o seu próprio documento
Abra o contrato marcado no playground do TypeSafe para editar as
perguntas contra o mesmo texto. Para buscar o seu próprio, troque a URL em
fetch_document(); todas as outras linhas do script trabalham a partir de LINES.