155 lines
4.8 KiB
JavaScript
155 lines
4.8 KiB
JavaScript
import Anthropic from "@anthropic-ai/sdk";
|
|
|
|
const GENERATION_MODEL = "claude-opus-4-6";
|
|
const NLI_MODEL = "claude-haiku-4-5";
|
|
|
|
|
|
const RESPONSE_SCHEMA = {
|
|
type: "object",
|
|
properties: {
|
|
answer: {
|
|
type: "string",
|
|
description:
|
|
"The answer with inline numeric citations like [1], [2] matching the source document numbers provided",
|
|
},
|
|
citedSourceIndices: {
|
|
type: "array",
|
|
items: { type: "integer" },
|
|
description: "The document numbers cited in the answer",
|
|
},
|
|
followUpQuestions: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
description: "Exactly 3 follow-up questions the user might ask",
|
|
},
|
|
},
|
|
required: ["answer", "citedSourceIndices", "followUpQuestions"],
|
|
additionalProperties: false,
|
|
};
|
|
|
|
const NLI_SCHEMA = {
|
|
type: "object",
|
|
properties: {
|
|
label: {
|
|
type: "string",
|
|
enum: ["supported", "contradicted", "unsupported"],
|
|
description: "Whether the claim is entailed, contradicted, or neither by the passages",
|
|
},
|
|
confidence: {
|
|
type: "number",
|
|
description: "Confidence in the label, from 0 to 1",
|
|
},
|
|
},
|
|
required: ["label", "confidence"],
|
|
additionalProperties: false,
|
|
};
|
|
|
|
function getClient() {
|
|
const key = process.env.ANTHROPIC_API_KEY;
|
|
if (!key) throw new Error("ANTHROPIC_API_KEY is not set");
|
|
return new Anthropic({ apiKey: key });
|
|
}
|
|
|
|
function textFromMessage(message) {
|
|
const block = message.content[0];
|
|
return block && "text" in block ? block.text : "";
|
|
}
|
|
|
|
function buildPrompt(query, sources) {
|
|
const today = new Date().toLocaleDateString("en-US", {
|
|
weekday: "long",
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
});
|
|
|
|
const sourceBlock = sources
|
|
.map((s) => `[Source ${s.docIndex}] (${s.name})\n${s.text}`)
|
|
.join("\n\n---\n\n");
|
|
|
|
return `You are a research assistant. Today's date is ${today}. Answer the user's question using ONLY the source documents provided below. Follow these rules strictly:
|
|
|
|
1. Ground every claim in a specific source document. Cite sources inline using numeric notation like [1], [2], etc., matching the source document numbers below.
|
|
2. If the sources do not contain enough information to answer, say so honestly.
|
|
3. After your answer, suggest exactly 3 follow-up questions the user might ask based on the sources.
|
|
|
|
--- SOURCE DOCUMENTS ---
|
|
|
|
${sourceBlock}
|
|
|
|
--- USER QUESTION ---
|
|
|
|
${query}`;
|
|
}
|
|
|
|
function stripInvalidCitations(answer, validIndices) {
|
|
return answer
|
|
.replace(/\[(\d+)\]/g, (match, n) => (validIndices.has(Number(n)) ? match : ""))
|
|
.replace(/[ \t]{2,}/g, " ")
|
|
.trim();
|
|
}
|
|
|
|
export async function generate(query, sources) {
|
|
const client = getClient();
|
|
const message = await client.messages.create({
|
|
model: GENERATION_MODEL,
|
|
max_tokens: 2048,
|
|
messages: [{ role: "user", content: buildPrompt(query, sources) }],
|
|
output_config: {
|
|
format: {
|
|
type: "json_schema",
|
|
schema: RESPONSE_SCHEMA,
|
|
},
|
|
},
|
|
});
|
|
|
|
const parsed = JSON.parse(textFromMessage(message) || "{}");
|
|
const validIndices = new Set(sources.map((s) => s.docIndex));
|
|
const citedSourceIndices = [
|
|
...new Set((parsed.citedSourceIndices || []).filter((idx) => validIndices.has(idx))),
|
|
];
|
|
|
|
return {
|
|
answer: stripInvalidCitations(parsed.answer || "", validIndices),
|
|
citedSourceIndices,
|
|
followUpQuestions: parsed.followUpQuestions || [],
|
|
};
|
|
}
|
|
|
|
export async function judgeEntailment(sentence, chunkTexts) {
|
|
const client = getClient();
|
|
const passages = chunkTexts.map((t, i) => `[Passage ${i + 1}]\n${t}`).join("\n\n");
|
|
|
|
const prompt = `You are an NLI judge. Decide whether the CLAIM is supported, contradicted, or unsupported by the SOURCE PASSAGES.
|
|
|
|
- supported: the passages entail the claim
|
|
- contradicted: the passages contradict the claim
|
|
- unsupported: the passages neither support nor contradict the claim
|
|
|
|
--- CLAIM ---
|
|
${sentence}
|
|
|
|
--- SOURCE PASSAGES ---
|
|
${passages}`;
|
|
|
|
const message = await client.messages.create({
|
|
model: NLI_MODEL,
|
|
max_tokens: 256,
|
|
messages: [{ role: "user", content: prompt }],
|
|
output_config: {
|
|
format: {
|
|
type: "json_schema",
|
|
schema: NLI_SCHEMA,
|
|
},
|
|
},
|
|
});
|
|
|
|
const parsed = JSON.parse(textFromMessage(message) || "{}");
|
|
const label = ["supported", "contradicted", "unsupported"].includes(parsed.label)
|
|
? parsed.label
|
|
: "unsupported";
|
|
const confidence = typeof parsed.confidence === "number" ? parsed.confidence : 0;
|
|
|
|
return { label, confidence };
|
|
}
|