JevCode / Ökosystem-Beispiele

Zeilenweise Suche

Erstellen Sie semantische Suche für GitHub's Terms of Service. Werten Sie in einer Anfrage 218 line ids gegen eine plain-language query mit einer Choice question aus und verwenden Sie eine Noul question, um zu prüfen, ob das Dokument eine Antwort enthält.

Maschinell aus en übersetzt, nicht lektoriert. Nur als Kurzreferenz geeignet.

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

Du hast die Nutzungsbedingungen von GitHub und eine Frage in einfacher Sprache dazu. Du benötigst die Zeilen, die die Frage beantworten, sowie eine Möglichkeit zu erkennen, wenn das Dokument keine Antwort enthält. Die enthaltenen Abfragen sortieren Zeilen mit direkten Antworten an erster Stelle. Die exists-Schwellenwerte klassifizieren die verbleibenden Fälle als fehlend oder unvollständig. Du erhältst find(), das die exists-Wahrscheinlichkeit und einen Relevanzscore pro Zeile zurückgibt.

Eine Abfrage durchsucht ein Dokument und zeigt eine Antwort an, die der passenden
Zeile zugeordnet ist

Das Search-Backend setzt sich aus drei Teilen zusammen:

  1. Markiere jede Zeile mit einer ID, damit TypeSafe sie referenzieren kann.
  2. Verwende eine Choice-Frage, um diese Zeilen-IDs danach zu rangieren, wie gut sie die Anfrage beantworten. Choice-Fragen-Wahrscheinlichkeiten summieren sich immer zu 1, sodass eine Zeile auch dann auf Rang 1 landet, wenn keine die Anfrage beantwortet.
  3. Verwende in derselben Anfrage eine Noul-Frage, um zu prüfen, ob das Dokument überhaupt eine Antwort enthält.

Setup

Holen Sie sich einen TypeSafe-API-Schlüssel

Erstellen Sie einen Schlüssel in der TypeSafe-Konsole und exportieren Sie ihn:

export TYPESAFE_API_KEY="your-key-here"

Abhängigkeiten installieren

pip install "typesafe-sdk>=0.5.7" cooksafe \
  --extra-index-url https://pypi.typesafe.ai/

JsonCache spielt die enthaltenen API-Antworten ab, sodass die folgenden Schritte ohne API-Schlüssel oder Kosten ausgeführt werden. Um die Anfragen live zu stellen, setzen Sie TYPESAFE_API_KEY und löschen json_cache.json.

Skript erstellen

Start semantic_search.py mit den Imports und dem 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"))

Schritt 1: Tagge jede Zeile mit einer ID

Das Testdokument sind die Nutzungsbedingungen von GitHub, aufgeteilt in 218 Klauseln, sodass jedes Suchergebnis auf eine zitierfähige Zeile verweist.

Zu semantic_search.py hinzufügen:

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()

Der Cache verhindert wiederholte Downloads, und splitlines() hinterlässt eine Liste von 218 Strings.

Jetzt jede Zeile mit einer kurzen ID präfixen und die Zeilen wieder zu einem Dokument zusammenfügen. Das Modell verwendet diese IDs, um auf seine Antwort zu verweisen.

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 sieht jetzt so aus:

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

Schritt 2: Fragen, wo die Antwort ist

Eine Choice Frage gibt für jede Option eine Wahrscheinlichkeit zurück. Verwenden Sie die Zeilen-IDs als Optionen, und „eine Option auswählen“ wird zu „auf eine Zeile zeigen“.

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))},
    )

Die Optionsbeschreibungen sind None, da das Dokument den Text für jede ID bereits enthält. Die Abfrage erfolgt in instructions; der Zustand bleibt zwischen den Suchen unverändert.

Hinweis — Eine Choice-Frage akzeptiert bis zu 255 Optionen, daher durchsucht dieses Rezept Dokumente mit bis zu 255 Zeilen in einer einzigen Anfrage. Darüber hinaus suchen Sie in zwei Durchgängen: Eine Choice-Frage wählt einen Zeilenbereich aus, und eine zweite Rangfolge bewertet die Zeilen darin.

Schritt 3: Prüfen, ob eine Antwort existiert

Die Wahrscheinlichkeiten der Choice-Werte summieren sich immer zu 1, sodass einige Zeilenranglisten an erster Stelle stehen, auch wenn das Dokument die Frage nicht beantwortet. Die Rangliste allein kann keine echte Antwort von der nächstgelegenen irrelevanten Zeile unterscheiden.

Stelle also eine zweite Frage in derselben Anfrage:

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",
        ),
    )

Im Gegensatz zu den Choice-Wahrscheinlichkeiten hängt die Noul-Wahrscheinlichkeit nicht von den anderen Optionen ab, daher kann sie nahe bei Null liegen, wenn das Dokument keine Antwort enthält.

Schritt 4: Senden Sie beide Fragen in einer Anfrage

Die system_one-Methode beantwortet beide Fragen in einem Durchlauf. Der Zustand wird einmal gesendet, sodass das Hinzufügen der Existenzprüfung nur eine geringe Menge an zusätzlichem Output erfordert.

Ein markiertes Dokument und eine Benutzerfrage geben eine TypeSafe-Anfrage ein. Eine Choice-Frage bewertet
jede Zeile, während eine Noul-Frage prüft, ob eine Antwort existiert. Lokaler Code rangiert dann die
Zeilen und wendet das Dokumenturteil an.

@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),
    )

Die relevance-Liste führt einen Score pro Zeile in Dokumentreihenfolge auf.

Schritt 5: das Ergebnis lesen

Zwei lokale Code-Snippets erledigen die Arbeit: verdict() wandelt die rohe exists-Wahrscheinlichkeit in drei Zustände um, wobei ein mittlerer Zustand für partielle Antworten vorgesehen ist, und show() stellt relevance als Balkendiagramm dar, sodass die Rangfolge in einem Terminal ablesbar ist.

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

Diese Schwellenwerte trennen die folgenden Beispiele, sollten aber vor dem Einsatz in der Produktion an Ihre eigenen Dokumente angepasst werden.

Schritt 6: die Suche ausführen

Stelle zwei Fragen, die direkte Antworten haben, eine Frage, die keine Antwort hat, und eine Frage, die eine teilweise Antwort hat, insgesamt vier.

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,

Was die Werte bedeuten

Die ersten beiden Abfragen liefern direkte Antworten und die zur Verifizierung benötigten Quellzeilen.

Die anderen beiden zeigen, warum die Existenzprüfung wichtig ist:

  • Schiedsverfahren: Die Rangliste gibt der nächsten Zeile eine Punktzahl von 0,86, aber exists ist nur 0,14. Die Antwort ist nicht im Dokument enthalten.
  • Elterliche Erlaubnis: Die Altersregel rangiert an erster Stelle, aber sie beantwortet nicht, ob die elterliche Erlaubnis die Regel ändert. Das Ergebnis ist teilweise behandelt.

Die Rangliste sagt Ihnen, wo Sie suchen müssen; die exists-Punktzahl sagt Ihnen, ob das Ergebnis die Frage beantwortet.

Probieren Sie es mit Ihrem eigenen Dokument aus

Öffne den markierten Vertrag im TypeSafe-Playground, um die Fragen gegen denselben Text zu bearbeiten. Um deine eigenen zu durchsuchen, tausche die URL in fetch_document(); jede andere Zeile des Skripts arbeitet mit LINES.