Zeilenweise Suche
Baue eine semantische Suche für GitHubs Nutzungsbedingungen. Bewerte in einer Anfrage 218 Zeilen-IDs mit einer Choice-Frage gegen eine alltagssprachliche Suchanfrage und prüfe mit einer Noul-Frage, ob das Dokument überhaupt eine Antwort enthält.
Du hast GitHubs Nutzungsbedingungen und eine alltagssprachliche Frage dazu. Du brauchst die
Zeilen, die die Frage beantworten, und eine Möglichkeit zu erkennen, wann das Dokument keine
Antwort hat. Die mitgelieferten Suchanfragen reihen Zeilen mit direkten Antworten zuerst ein. Die
exists-Schwellenwerte ordnen die übrigen Fälle als fehlend oder teilweise ein. Am Ende hast du
find(), das die exists-Wahrscheinlichkeit und einen Relevanz-Score pro Zeile zurückgibt.
Das Such-Backend besteht aus drei Teilen:
- Versehe jede Zeile mit einer ID, damit TypeSafe darauf verweisen kann.
- Nutze eine
Choice-Frage, um diese Zeilen-IDs danach zu reihen, wie gut sie die Suche beantworten. Die Wahrscheinlichkeiten einer Choice-Frage addieren sich immer zu 1, also steht eine Zeile an erster Stelle, selbst wenn keine die Suchanfrage beantwortet. - Nutze in derselben Anfrage eine
Noul-Frage, um zu prüfen, ob das Dokument überhaupt eine Antwort enthält.
Einrichtung
Einen TypeSafe-API-Schlüssel bekommen
Erstelle einen Schlüssel in der TypeSafe-Konsole und exportiere ihn:
export TYPESAFE_API_KEY="your-key-here"
Abhängigkeiten installieren
pip install 'cooksafe>=0.2.0,<0.3.0'
JsonCache spielt die mitgelieferten API-Antworten erneut ab, sodass die folgenden Schritte
ohne API-Schlüssel und ohne Kosten laufen. Um die Anfragen stattdessen live zu stellen, setze
TYPESAFE_API_KEY und lösche json_cache.json.
Das Skript erstellen
Beginne 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: Jede Zeile mit einer ID versehen
Das Testdokument sind GitHubs Nutzungsbedingungen, in 218 Klauseln aufgeteilt, sodass jedes Suchergebnis auf eine zitierbare Zeile verweist.
Füge zu semantic_search.py hinzu:
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 aus 218
Zeichenketten.
Stelle nun jeder Zeile eine kurze ID voran und füge die Zeilen wieder zu einem Dokument zusammen. Das Modell nutzt 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 steht
Eine Choice-Frage gibt für jede Option eine Wahrscheinlichkeit zurück. Nutze die Zeilen-IDs
als Optionen,
und aus „wähle eine Option“ wird „verweise auf eine Zeile“.
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, weil das Dokument den Text für jede ID bereits enthält.
Die Suchanfrage kommt in instructions; der Zustand bleibt zwischen den Suchen unverändert.
Schritt 3: Prüfen, ob eine Antwort existiert
Die Wahrscheinlichkeiten einer Choice-Frage addieren sich immer zu 1, also steht irgendeine Zeile an erster Stelle, selbst wenn das Dokument die Frage nicht beantwortet. Die Reihung allein kann eine echte Antwort nicht von der nächstbesten 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",
),
)
Anders als die Choice-Wahrscheinlichkeiten hängt die Noul-Wahrscheinlichkeit nicht von den anderen Optionen ab, also kann sie nahe null fallen, wenn das Dokument keine Antwort hat.
Schritt 4: Beide Fragen in einer Anfrage senden
Die Methode system_one beantwortet beide Fragen in einem Durchgang. Der Zustand wird einmal
gesendet, also erfordert die zusätzliche Existenzprüfung nur wenig zusätzliche Ausgabe.
@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 Liste relevance hält einen Score pro Zeile, in Dokumentreihenfolge.
Schritt 5: Das Ergebnis lesen
Zwei Stücke lokalen Codes erledigen den Rest: verdict() verwandelt die rohe
exists-Wahrscheinlichkeit
in drei Zustände, mit einem mittleren für teilweise Antworten, und show() rendert relevance
als Balkendiagramm, damit die Reihung im Terminal lesbar 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 Beispiele unten, aber stimme sie auf deine eigenen Dokumente ab, bevor du sie in Produktion einsetzt.
Schritt 6: Die Suche ausführen
Stelle zwei Fragen, die direkte Antworten haben, eine, die keine Antwort hat, und eine, 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 Scores bedeuten
Die ersten beiden Suchanfragen liefern direkte Antworten und die Quellzeilen, die man zum Überprüfen braucht.
Die anderen beiden zeigen, warum die Existenzprüfung wichtig ist:
- Schiedsverfahren: Die Reihung gibt der nächstbesten Zeile einen Score von 0.86, aber
existsist nur 0.14. Die Antwort steht nicht im Dokument. - Elterliche Erlaubnis: Die Altersregel steht an erster Stelle, aber sie beantwortet nicht, ob eine elterliche Erlaubnis die Regel ändert. Das Ergebnis ist teilweise beantwortet.
Die Reihung sagt dir, wo du suchen sollst; der exists-Score sagt dir, ob das Ergebnis die
Frage beantwortet.
Mit deinem eigenen Dokument ausprobieren
Öffne den mit Tags versehenen Vertrag im TypeSafe-Playground, um die Fragen
gegen denselben Text zu bearbeiten. Um dein eigenes Dokument zu durchsuchen, tausche die URL
in fetch_document() aus; jede andere Zeile des Skripts arbeitet mit LINES.