Add shared library for APIs/chunking
This commit is contained in:
154
lib/anthropic.js
Normal file
154
lib/anthropic.js
Normal file
@@ -0,0 +1,154 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user