Chapter 6 · section 3 of 6
The rephraser
Listing: ask/rephrase.ts
import Anthropic from "@anthropic-ai/sdk";
import { fileURLToPath } from "node:url";
import { SIDE_JOB_TIMEOUT_MS, SMALL_MODEL, requireApiKey, textOf } from "./setup.js";
// The learner's question, said again in the words the law uses, so that
// retrieval can find the law. Retrieval ranks by words and by meaning, and
// both answer in the learner's vocabulary. Absent or failing, retrieval runs
// on the question alone, exactly as before.
/** At most this many searches: each is one more pair of rankings fused into the pool. */
export const REPHRASE_SEARCHES = 2;
export const REPHRASE_RULES = `A learner driver asked a question about their state's road law.
Write one or two short searches, in the words a state's vehicle code and driver manual use for the same thing, that would find the law that answers it.
Use the law's names for things: "maximum speed limit" for the highest limit, "instruction permit" or "learner's permit" for a permit, "financial responsibility" or "liability insurance" for insurance, "implied consent" for a breath test; a code never says "texting": a search for it says "electronic message or text message", because some codes use one and some the other, and "wireless communication device" or "handheld telephone" for a phone.
Keep the kind of figure asked about (feet, miles per hour, days, years, dollars) but never a figure.
When the question asks what happens, what if, will I, do I have to, or how much, write two searches: one for the duty, as a code states what a driver must or may not do ("have in possession", "display on demand"), and one for its penalty or defence in the words a code uses for them ("fine", "offense", "defense to prosecution", "produces in court", "points", "suspend").
Each search is at most eight words. Return no searches if the question is not about road law.`;
const SCHEMA = {
type: "object",
properties: { searches: { type: "array", items: { type: "string" } } },
required: ["searches"],
additionalProperties: false,
} as const;
/** Searches in the law's words for a learner's question, best first; empty when it is not road law. */
export type Rephrase = (question: string) => Promise<string[]>;
// A search's learner words that no code uses, said as the codes say them.
// Haiku is told not to write "texting" and still does in a third of its
// searches; this is what holds when it does.
export function inLawWords(search: string): string {
return search.replace(/\btext(?:s|ing|ed)?\b(?!\s+messag)/gi, "electronic message text message");
}
/** The client the side jobs use: a short timeout and no retries, so a hung call cannot hold a question. */
export function sideJobClient(): Anthropic {
return new Anthropic({ timeout: SIDE_JOB_TIMEOUT_MS, maxRetries: 0 });
}
// Throws on anything it cannot read, so the caller searches with the
// question alone.
export function haikuRephraseFor(client: Pick<Anthropic, "messages">): Rephrase {
return async (question) => {
const response = await client.messages.create({
model: SMALL_MODEL,
max_tokens: 1024,
system: REPHRASE_RULES,
output_config: {
effort: "low",
format: { type: "json_schema", schema: SCHEMA },
},
messages: [{ role: "user", content: question }],
});
const text = textOf(response);
const parsed = JSON.parse(text) as { searches?: unknown };
if (!Array.isArray(parsed.searches) || parsed.searches.some((search) => typeof search !== "string")) {
throw new Error(`the rephraser answered with no searches: ${text}`);
}
return (parsed.searches as string[])
.map((search) => search.trim())
.filter((search) => search.length > 0 && search.length <= 80)
.slice(0, REPHRASE_SEARCHES)
.map(inLawWords);
};
}
async function main(): Promise<void> {
requireApiKey();
const question = process.argv.slice(2).join(" ");
if (question.length === 0) {
console.error('usage: tsx ask/rephrase.ts "<question>"');
process.exit(1);
}
const rephrase = haikuRephraseFor(sideJobClient());
for (const search of await rephrase(question)) console.log(search);
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
} Run it:
npx tsx ask/rephrase.ts "whats the fastest i can go on a residential street with no sign"
npx tsx ask/rephrase.ts "what happens if i get pulled over without my license"
npx tsx ask/rephrase.ts "what is the capital of France" The first prints one or two searches in the code’s register, something like maximum speed limit residential district. The second prints two, one for the duty to carry a licence and one for its penalty. The third prints nothing. Run the first one three times and the wording changes a little each time, which is why the searches are fused into a list rather than trusted on their own.
Three parts of the file are the design.
It throws, and the caller is expected to catch. A rephraser that returns an empty list on a parse failure would look, to the caller, like a rephraser that read the question as not being about road law. Those are different things, and the log should say which happened. So on anything it cannot read the function throws, and the pipeline catches, logs the rephraser did not answer, and retrieves on the question alone, which is Chapter 5’s pipeline unchanged.
The leash is in the client. sideJobClient builds a client with SIDE_JOB_TIMEOUT_MS from Chapter 1 and no retries. The rephraser runs before every question, and a question is a learner waiting; the SDK’s default of ten minutes retried twice is the wrong default for that, and every side job overrides it. The answering model keeps the default, because by the time it is called the pipeline has nothing to fall back to.
inLawWords holds what the prompt asks for. The rules tell the model that a code never says texting, and in a third of its searches it wrote texting anyway. The function rewrites the word to both forms the codes use, so a search finds Texas’s “electronic message” and Maryland’s “text message” alike. A rule in a prompt is a request, and when it matters, a line of code stands behind it.