JevCode / Casos do ecossistema

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.

Fonte: docs.typesafe.ai/cookbooks/semantic_findcookbookrecipe
Locating the lines that answer a query in a long document

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.

Uma consulta analisa um documento e revela uma resposta anexada à linha correspondente

O backend de busca é composto por três partes:

  1. Tag each line with an ID so TypeSafe can point to it.
  2. Use a Choice question 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.
  3. In the same request, use a Noul question 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 Choice aceita 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.

Um documento etiquetado e a pergunta do usuário entram em uma única solicitação TypeSafe. Uma pergunta Choice pontua
cada linha enquanto uma pergunta Noul verifica se uma resposta existe. O código local então classifica as
linhas e aplica o veredito do documento.

@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.