Joel Stevick

Appendix B · section 7 of 32

ask/scope.ts

From Talking to a Model.

import Anthropic from "@anthropic-ai/sdk";
import { ANSWER_MODEL, requireApiKey, textOf } from "./setup.js";

requireApiKey();

const client = new Anthropic();

// The only words the model may answer with. An enum in the schema is a
// promise the API keeps: the reply is one of these or the request fails.
const SCOPES = ["road-law", "other"] as const;
type Scope = (typeof SCOPES)[number];

const SCHEMA = {
  type: "object",
  properties: {
    scope: { type: "string", enum: [...SCOPES] },
    why: { type: "string" },
  },
  required: ["scope", "why"],
  additionalProperties: false,
} as const;

const RULES = [
  "You sort a learner driver's question into one of two scopes.",
  'A rule of the road, a sign, a speed limit, a fine, an age, a document, or a',
  'requirement of a particular state is "road-law".',
  'Anything else - the app, the weather, the capital of France - is "other".',
  "Say why in one short sentence.",
].join(" ");

async function scopeOf(question: string): Promise<{ scope: Scope; why: string } | null> {
  const response = await client.messages.create({
    model: ANSWER_MODEL,
    max_tokens: 1024,
    system: RULES,
    output_config: {
      effort: "low",
      format: { type: "json_schema", schema: SCHEMA },
    },
    messages: [{ role: "user", content: question }],
  });

  // A refusal is a 200 with no answer in it. Check it before reading.
  if (response.stop_reason === "refusal") return null;

  const parsed = JSON.parse(textOf(response)) as { scope?: unknown; why?: unknown };
  if (
    typeof parsed.scope !== "string" ||
    !(SCOPES as readonly string[]).includes(parsed.scope) ||
    typeof parsed.why !== "string"
  ) {
    return null;
  }
  return { scope: parsed.scope as Scope, why: parsed.why };
}

async function main(): Promise<void> {
  const question = process.argv.slice(2).join(" ");
  if (question.length === 0) {
    console.error('usage: tsx ask/scope.ts "<question>"');
    process.exit(1);
  }
  const sorted = await scopeOf(question);
  console.log(sorted === null ? "no usable reply" : `${sorted.scope}: ${sorted.why}`);
}

main().catch((error: unknown) => {
  console.error(error instanceof Error ? error.message : String(error));
  process.exit(1);
});