Add shared library for APIs/chunking #1
6
.env.example
Normal file
6
.env.example
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
VOYAGE_API_KEY=
|
||||||
|
ANTHROPIC_API_KEY=
|
||||||
|
PINECONE_API_KEY=
|
||||||
|
PINECONE_INDEX_NAME=
|
||||||
|
# rerank | rerank_nli (default rerank_nli)
|
||||||
|
GROUNDEDNESS_MODE=rerank_nli
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
|
secrets
|
||||||
66
README.md
Normal file
66
README.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# RAG Lambdas - Reliable, Source Grounded Serverless Retrevial Augmented Generation Pipeline
|
||||||
|
|
||||||
|
AWS Lambda functions for source-grounded Q&A: ingest documents from S3 to Pinecone, answer questions with citations and a groundedness score.
|
||||||
|
|
||||||
|
Embeddings and reranking use the Voyage API; generation and NLI use Anthropic. Lambdas need in/outbound HTTPS to `api.voyageai.com` and `api.anthropic.com` for REST API interactions.
|
||||||
|
|
||||||
|
## Pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
ingest:
|
||||||
|
S3 object
|
||||||
|
→ overlapping ~2000-char chunks
|
||||||
|
→ Voyage voyage-3 (input_type=document)
|
||||||
|
→ Pinecone upsert { text, source, chunkIndex }
|
||||||
|
|
||||||
|
query:
|
||||||
|
question
|
||||||
|
→ Voyage voyage-3 (input_type=query)
|
||||||
|
→ Pinecone top-20 (cosine)
|
||||||
|
→ Voyage rerank-2 → top-5
|
||||||
|
→ Claude opus-4-6 (JSON answer + citations + follow-ups)
|
||||||
|
→ strip citation indices that are not in the retrieved set
|
||||||
|
→ groundedness:
|
||||||
|
A. per sentence, rerank-2(sentence, cited chunk texts) → max score
|
||||||
|
B. optional Haiku NLI: supported | contradicted | unsupported
|
||||||
|
C. linear ramp on rerank scores (0.2–0.8, not cosine 0.35–0.65)
|
||||||
|
→ { answer, citations, groundednessScore, followUpQuestions, contextUsed }
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Groundedness is not bi-encoder cosine. Cosine is used only for Pinecone ANN retrieval. The badge is Voyage `rerank-2` over cited chunk text, with an optional Haiku entailment pass.
|
||||||
|
|
||||||
|
## Optional Rerank
|
||||||
|
|
||||||
|
`GROUNDEDNESS_MODE=rerank` skips the Haiku LLM-judge (less expensive/lower latency). `rerank_nli` (default) runs both.
|
||||||
|
|
||||||
|
Raw per-sentence rerank scores are logged as `sentence_groundedness` (for planned isotonic calibrator fit to labeled pairs - future update).
|
||||||
|
|
||||||
|
## Pinecone index
|
||||||
|
|
||||||
|
Voyage-3 embeddings are **1024-dimensional**. Create a new index, then ingest.
|
||||||
|
|
||||||
|
- metric: `cosine`
|
||||||
|
- dimension: `1024`
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
Copy `.env.example` and set:
|
||||||
|
|
||||||
|
| Variable | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `VOYAGE_API_KEY` | voyage-3 embed + rerank-2 |
|
||||||
|
| `ANTHROPIC_API_KEY` | opus-4-6 generation + Haiku NLI |
|
||||||
|
| `PINECONE_API_KEY` | vector store |
|
||||||
|
| `PINECONE_INDEX_NAME` | 1024-d cosine index |
|
||||||
|
| `GROUNDEDNESS_MODE` | `rerank` or `rerank_nli` |
|
||||||
|
|
||||||
|
## Lambdas
|
||||||
|
|
||||||
|
- `ingest.js` — S3 trigger. Chunks, embeds, upserts.
|
||||||
|
- `query.js` — API Gateway / HTTP. Body: `{ "question": "..." }`.
|
||||||
|
|
||||||
|
Shared code lives in `lib/`. Package both handlers with `node_modules` (Node 18+, ESM: `"type": "module"` in `package.json`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
33
ingest.js
33
ingest.js
@@ -1,10 +1,11 @@
|
|||||||
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
|
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
|
||||||
import { BedrockRuntimeClient, InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime";
|
|
||||||
import { Pinecone } from "@pinecone-database/pinecone";
|
import { Pinecone } from "@pinecone-database/pinecone";
|
||||||
|
import { chunkText } from "./lib/chunk.js";
|
||||||
|
import { embedTexts } from "./lib/voyage.js";
|
||||||
|
|
||||||
const s3 = new S3Client({});
|
const s3 = new S3Client({});
|
||||||
const bedrock = new BedrockRuntimeClient({});
|
|
||||||
const pc = new Pinecone({ apiKey: process.env.PINECONE_API_KEY });
|
const pc = new Pinecone({ apiKey: process.env.PINECONE_API_KEY });
|
||||||
|
const UPSERT_BATCH_SIZE = 100;
|
||||||
|
|
||||||
export const handler = async (event) => {
|
export const handler = async (event) => {
|
||||||
const bucket = event.Records[0].s3.bucket.name;
|
const bucket = event.Records[0].s3.bucket.name;
|
||||||
@@ -13,28 +14,22 @@ export const handler = async (event) => {
|
|||||||
const s3Response = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
const s3Response = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
||||||
const rawText = await s3Response.Body.transformToString();
|
const rawText = await s3Response.Body.transformToString();
|
||||||
|
|
||||||
const chunks = rawText.match(/[\s\S]{1,500}/g) || [];
|
const chunks = chunkText(rawText);
|
||||||
|
if (chunks.length === 0) {
|
||||||
|
return { status: "Success", processedChunks: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const embeddings = await embedTexts(chunks, "document");
|
||||||
const index = pc.index(process.env.PINECONE_INDEX_NAME);
|
const index = pc.index(process.env.PINECONE_INDEX_NAME);
|
||||||
|
|
||||||
for (let i = 0; i < chunks.length; i++) {
|
const vectors = chunks.map((chunk, i) => ({
|
||||||
const chunk = chunks[i];
|
id: `${key}_chunk_${i}`,
|
||||||
|
values: embeddings[i],
|
||||||
const bedrockResponse = await bedrock.send(new InvokeModelCommand({
|
metadata: { text: chunk, source: key, chunkIndex: i },
|
||||||
modelId: "amazon.titan-embed-text-v1",
|
|
||||||
contentType: "application/json",
|
|
||||||
accept: "application/json",
|
|
||||||
body: JSON.stringify({ inputText: chunk })
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { embedding } = JSON.parse(new TextDecoder().decode(bedrockResponse.body));
|
for (let i = 0; i < vectors.length; i += UPSERT_BATCH_SIZE) {
|
||||||
|
await index.upsert(vectors.slice(i, i + UPSERT_BATCH_SIZE));
|
||||||
// 4. Upsert into Vector Database
|
|
||||||
await index.upsert([{
|
|
||||||
id: `${key}_chunk_${i}`,
|
|
||||||
values: embedding,
|
|
||||||
metadata: { text: chunk, source: key }
|
|
||||||
}]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { status: "Success", processedChunks: chunks.length };
|
return { status: "Success", processedChunks: chunks.length };
|
||||||
|
|||||||
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 };
|
||||||
|
}
|
||||||
32
lib/chunk.js
Normal file
32
lib/chunk.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const CHUNK_SIZE = 2000;
|
||||||
|
const CHUNK_OVERLAP = 200;
|
||||||
|
|
||||||
|
export function chunkText(text) {
|
||||||
|
const cleaned = String(text || "").replace(/\r\n/g, "\n").trim();
|
||||||
|
if (!cleaned) return [];
|
||||||
|
if (cleaned.length <= CHUNK_SIZE) return [cleaned];
|
||||||
|
|
||||||
|
const chunks = [];
|
||||||
|
let start = 0;
|
||||||
|
|
||||||
|
while (start < cleaned.length) {
|
||||||
|
let end = Math.min(start + CHUNK_SIZE, cleaned.length);
|
||||||
|
|
||||||
|
if (end < cleaned.length) {
|
||||||
|
const slice = cleaned.slice(start, end);
|
||||||
|
const lastBreak = Math.max(slice.lastIndexOf("\n"), slice.lastIndexOf(" "));
|
||||||
|
if (lastBreak > CHUNK_SIZE * 0.5) {
|
||||||
|
end = start + lastBreak;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunk = cleaned.slice(start, end).trim();
|
||||||
|
if (chunk) chunks.push(chunk);
|
||||||
|
if (end >= cleaned.length) break;
|
||||||
|
|
||||||
|
const nextStart = end - CHUNK_OVERLAP;
|
||||||
|
start = nextStart <= start ? end : nextStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
27
lib/chunk.test.js
Normal file
27
lib/chunk.test.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { describe, it } from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { chunkText } from "./chunk.js";
|
||||||
|
|
||||||
|
describe("chunkText", () => {
|
||||||
|
it("returns empty array for blank input", () => {
|
||||||
|
assert.deepEqual(chunkText(""), []);
|
||||||
|
assert.deepEqual(chunkText(" "), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps short text as a single chunk", () => {
|
||||||
|
const text = "A short paragraph.";
|
||||||
|
assert.deepEqual(chunkText(text), [text]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("splits long text into overlapping chunks under 2000 chars", () => {
|
||||||
|
const word = "word ";
|
||||||
|
const text = word.repeat(900);
|
||||||
|
const chunks = chunkText(text);
|
||||||
|
assert.ok(chunks.length > 1);
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
assert.ok(chunk.length <= 2000);
|
||||||
|
}
|
||||||
|
const overlap = chunks[0].slice(-50);
|
||||||
|
assert.ok(chunks[1].includes(overlap.trim().slice(0, 20)));
|
||||||
|
});
|
||||||
|
});
|
||||||
77
lib/scoring.js
Normal file
77
lib/scoring.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { rerank } from "./voyage.js";
|
||||||
|
import { judgeEntailment } from "./anthropic.js";
|
||||||
|
|
||||||
|
// These bounds are a starting ramp for rerank-2; log raw scores and adjust.
|
||||||
|
const RERANK_FLOOR = 0.2;
|
||||||
|
const RERANK_CEILING = 0.8;
|
||||||
|
const MIN_SENTENCE_LENGTH = 20;
|
||||||
|
const MAX_SENTENCES = 12;
|
||||||
|
const MAX_CITED_CHUNKS = 5;
|
||||||
|
|
||||||
|
function splitIntoSentences(text) {
|
||||||
|
const cleaned = text.replace(/\[\d+\]/g, "").trim();
|
||||||
|
const raw = cleaned.split(/(?<=[.!?])\s+/);
|
||||||
|
return raw
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s.length >= MIN_SENTENCE_LENGTH)
|
||||||
|
.slice(0, MAX_SENTENCES);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calibrate(rawScore) {
|
||||||
|
const scaled = (rawScore - RERANK_FLOOR) / (RERANK_CEILING - RERANK_FLOOR);
|
||||||
|
return Math.max(0, Math.min(1, scaled));
|
||||||
|
}
|
||||||
|
|
||||||
|
function groundednessMode() {
|
||||||
|
const mode = (process.env.GROUNDEDNESS_MODE || "rerank_nli").toLowerCase();
|
||||||
|
return mode === "rerank" ? "rerank" : "rerank_nli";
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function computeGroundedness(answerText, citedChunkTexts) {
|
||||||
|
if (!citedChunkTexts || citedChunkTexts.length === 0) return 0;
|
||||||
|
|
||||||
|
const sentences = splitIntoSentences(answerText);
|
||||||
|
if (sentences.length === 0) return 0;
|
||||||
|
|
||||||
|
const chunks = citedChunkTexts.filter(Boolean).slice(0, MAX_CITED_CHUNKS);
|
||||||
|
if (chunks.length === 0) return 0;
|
||||||
|
|
||||||
|
const mode = groundednessMode();
|
||||||
|
|
||||||
|
const perSentence = await Promise.all(
|
||||||
|
sentences.map(async (sentence) => {
|
||||||
|
const ranked = await rerank(sentence, chunks);
|
||||||
|
const raw =
|
||||||
|
ranked.length === 0
|
||||||
|
? 0
|
||||||
|
: Math.max(...ranked.map((r) => r.relevanceScore));
|
||||||
|
const calibrated = calibrate(raw);
|
||||||
|
|
||||||
|
let nli = null;
|
||||||
|
if (mode === "rerank_nli") {
|
||||||
|
nli = await judgeEntailment(sentence, chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
event: "sentence_groundedness",
|
||||||
|
sentence: sentence.slice(0, 80),
|
||||||
|
rawRerank: Number(raw.toFixed(4)),
|
||||||
|
calibrated: Number(calibrated.toFixed(4)),
|
||||||
|
nli,
|
||||||
|
mode,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return { raw, calibrated, nli };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (perSentence.some((s) => s.nli && s.nli.label === "contradicted")) {
|
||||||
|
console.log(JSON.stringify({ event: "groundedness_floored", reason: "contradicted" }));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = perSentence.reduce((sum, s) => sum + s.calibrated, 0);
|
||||||
|
return total / perSentence.length;
|
||||||
|
}
|
||||||
66
lib/voyage.js
Normal file
66
lib/voyage.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
const VOYAGE_API_URL = "https://api.voyageai.com/v1";
|
||||||
|
const EMBED_MODEL = "voyage-3";
|
||||||
|
const RERANK_MODEL = "rerank-2";
|
||||||
|
const EMBED_BATCH_SIZE = 32;
|
||||||
|
|
||||||
|
function getApiKey() {
|
||||||
|
const key = process.env.VOYAGE_API_KEY;
|
||||||
|
if (!key) throw new Error("VOYAGE_API_KEY is not set");
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function voyagePost(path, body) {
|
||||||
|
const res = await fetch(`${VOYAGE_API_URL}${path}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${getApiKey()}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(`Voyage ${path} failed (${res.status}): ${text}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embed text - voyage-3. inputType must be "document" (ingest) or "query" (search).
|
||||||
|
|
||||||
|
export async function embedTexts(texts, inputType = "document") {
|
||||||
|
if (texts.length === 0) return [];
|
||||||
|
|
||||||
|
const embeddings = [];
|
||||||
|
for (let i = 0; i < texts.length; i += EMBED_BATCH_SIZE) {
|
||||||
|
const batch = texts.slice(i, i + EMBED_BATCH_SIZE);
|
||||||
|
const json = await voyagePost("/embeddings", {
|
||||||
|
model: EMBED_MODEL,
|
||||||
|
input: batch,
|
||||||
|
input_type: inputType,
|
||||||
|
});
|
||||||
|
const ordered = (json.data || [])
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
||||||
|
embeddings.push(...ordered.map((d) => d.embedding));
|
||||||
|
}
|
||||||
|
return embeddings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function rerank(query, documents) {
|
||||||
|
if (!documents || documents.length === 0) return [];
|
||||||
|
|
||||||
|
const texts = documents.map((d) => (typeof d === "string" ? d : d.text));
|
||||||
|
const json = await voyagePost("/rerank", {
|
||||||
|
model: RERANK_MODEL,
|
||||||
|
query,
|
||||||
|
documents: texts,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (json.data || []).map((item) => ({
|
||||||
|
index: item.index,
|
||||||
|
document: documents[item.index],
|
||||||
|
relevanceScore: item.relevance_score,
|
||||||
|
}));
|
||||||
|
}
|
||||||
478
package-lock.json
generated
Normal file
478
package-lock.json
generated
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
{
|
||||||
|
"name": "rag-lambdas",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "rag-lambdas",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.78.0",
|
||||||
|
"@aws-sdk/client-s3": "^3.864.0",
|
||||||
|
"@pinecone-database/pinecone": "^6.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@anthropic-ai/sdk": {
|
||||||
|
"version": "0.78.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.78.0.tgz",
|
||||||
|
"integrity": "sha512-PzQhR715td/m1UaaN5hHXjYB8Gl2lF9UVhrrGrZeysiF6Rb74Wc9GCB8hzLdzmQtBd1qe89F9OptgB9Za1Ib5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"json-schema-to-ts": "^3.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"anthropic-ai-sdk": "bin/cli"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"zod": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/checksums": {
|
||||||
|
"version": "3.1000.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1000.27.tgz",
|
||||||
|
"integrity": "sha512-insWOqKKNUrbN/dohEG7BJ0U5GkyqhjbMb/NHNaLUtq+7my2M8C4EnZZZoxMmXRqCC+P9dEr+KyJA2JGGzoKLg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.977.7",
|
||||||
|
"@aws-sdk/types": "^3.974.3",
|
||||||
|
"@smithy/core": "^3.31.1",
|
||||||
|
"@smithy/types": "^4.16.1",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/client-s3": {
|
||||||
|
"version": "3.1109.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1109.0.tgz",
|
||||||
|
"integrity": "sha512-iPWzBeGkAe5H5+dBBGCOdIT4uMhpu12OK+nFnDzjvOChVnHIws4LeoG7Yl0kJHSRWZnNEkVcF4vQYTJny0e5xA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/checksums": "^3.1000.27",
|
||||||
|
"@aws-sdk/core": "^3.977.7",
|
||||||
|
"@aws-sdk/credential-provider-node": "^3.972.79",
|
||||||
|
"@aws-sdk/middleware-sdk-s3": "^3.972.73",
|
||||||
|
"@aws-sdk/signature-v4-multi-region": "^3.996.44",
|
||||||
|
"@aws-sdk/types": "^3.974.3",
|
||||||
|
"@smithy/core": "^3.31.1",
|
||||||
|
"@smithy/fetch-http-handler": "^5.6.13",
|
||||||
|
"@smithy/node-http-handler": "^4.9.13",
|
||||||
|
"@smithy/types": "^4.16.1",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/core": {
|
||||||
|
"version": "3.977.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.977.7.tgz",
|
||||||
|
"integrity": "sha512-I88Iov89NVmjSmJLKSv7Cn9M2J+a2942OkA8nZCbz+sl4ZeY4zEOcoLOrbt1GRfQ8zEQKnjAJdXixA3J/p1fDQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/types": "^3.974.3",
|
||||||
|
"@aws-sdk/xml-builder": "^3.972.38",
|
||||||
|
"@aws/lambda-invoke-store": "^0.3.0",
|
||||||
|
"@smithy/core": "^3.31.1",
|
||||||
|
"@smithy/signature-v4": "^5.6.12",
|
||||||
|
"@smithy/types": "^4.16.1",
|
||||||
|
"bowser": "^2.11.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/credential-provider-env": {
|
||||||
|
"version": "3.972.68",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.68.tgz",
|
||||||
|
"integrity": "sha512-2a20A/IdNOwUvaDq91iqqS7BA0XlNMfW3iLGZGZLJv0EbUqhSxB0PIx4rQQqssvWj1uXImb3/UCCdHz/+1dOiA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.977.7",
|
||||||
|
"@aws-sdk/types": "^3.974.3",
|
||||||
|
"@smithy/core": "^3.31.1",
|
||||||
|
"@smithy/types": "^4.16.1",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/credential-provider-http": {
|
||||||
|
"version": "3.972.70",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.70.tgz",
|
||||||
|
"integrity": "sha512-0yRem2Fs52r/Nn6UAqIlpjexfaYj8ziEozOe9tamtAVT/5bzFLKx8O2r7MaRqgS3hGKHIa1Jij9nKHSsNnb04A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.977.7",
|
||||||
|
"@aws-sdk/types": "^3.974.3",
|
||||||
|
"@smithy/core": "^3.31.1",
|
||||||
|
"@smithy/fetch-http-handler": "^5.6.13",
|
||||||
|
"@smithy/node-http-handler": "^4.9.13",
|
||||||
|
"@smithy/types": "^4.16.1",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/credential-provider-ini": {
|
||||||
|
"version": "3.973.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.13.tgz",
|
||||||
|
"integrity": "sha512-2M39DE02XpYYaSWYk/4AsImXYUU/1L2xmTMLUpMMWq7DfLv191/vCRy3baKtdr45AkJQyVgSjmuVOLm15SwrRQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.977.7",
|
||||||
|
"@aws-sdk/credential-provider-env": "^3.972.68",
|
||||||
|
"@aws-sdk/credential-provider-http": "^3.972.70",
|
||||||
|
"@aws-sdk/credential-provider-login": "^3.972.75",
|
||||||
|
"@aws-sdk/credential-provider-process": "^3.972.68",
|
||||||
|
"@aws-sdk/credential-provider-sso": "^3.973.12",
|
||||||
|
"@aws-sdk/credential-provider-web-identity": "^3.972.74",
|
||||||
|
"@aws-sdk/nested-clients": "^3.997.42",
|
||||||
|
"@aws-sdk/types": "^3.974.3",
|
||||||
|
"@smithy/core": "^3.31.1",
|
||||||
|
"@smithy/credential-provider-imds": "^4.4.16",
|
||||||
|
"@smithy/types": "^4.16.1",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/credential-provider-login": {
|
||||||
|
"version": "3.972.75",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.75.tgz",
|
||||||
|
"integrity": "sha512-jaTESuJlQsoUZ44f/i2puyPt8VlF/dMMJ9HM3cStYtk7eKX4N9UWi83OLixUkoOJH3BwWlPLCq9YIK9nfWhVBg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.977.7",
|
||||||
|
"@aws-sdk/nested-clients": "^3.997.42",
|
||||||
|
"@aws-sdk/types": "^3.974.3",
|
||||||
|
"@smithy/core": "^3.31.1",
|
||||||
|
"@smithy/types": "^4.16.1",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/credential-provider-node": {
|
||||||
|
"version": "3.972.79",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.79.tgz",
|
||||||
|
"integrity": "sha512-RIw5dof1EHkWubrZzPC941CDtnFG1iAXsxbFgLkhdYZXHc4icU13c/uxSMI0J5eUx9bxa7LjfpdjfClBB1QsDA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/credential-provider-env": "^3.972.68",
|
||||||
|
"@aws-sdk/credential-provider-http": "^3.972.70",
|
||||||
|
"@aws-sdk/credential-provider-ini": "^3.973.13",
|
||||||
|
"@aws-sdk/credential-provider-process": "^3.972.68",
|
||||||
|
"@aws-sdk/credential-provider-sso": "^3.973.12",
|
||||||
|
"@aws-sdk/credential-provider-web-identity": "^3.972.74",
|
||||||
|
"@aws-sdk/types": "^3.974.3",
|
||||||
|
"@smithy/core": "^3.31.1",
|
||||||
|
"@smithy/credential-provider-imds": "^4.4.16",
|
||||||
|
"@smithy/types": "^4.16.1",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/credential-provider-process": {
|
||||||
|
"version": "3.972.68",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.68.tgz",
|
||||||
|
"integrity": "sha512-nLP3Pda2MQTFJ25hKBMmUuB9Uv+bTZQNlufbeCwklP549Vwnkd8bRLJoCKp5k6xjmdyptrPrOfGOhN0mKuca8A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.977.7",
|
||||||
|
"@aws-sdk/types": "^3.974.3",
|
||||||
|
"@smithy/core": "^3.31.1",
|
||||||
|
"@smithy/types": "^4.16.1",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/credential-provider-sso": {
|
||||||
|
"version": "3.973.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.12.tgz",
|
||||||
|
"integrity": "sha512-EmgyyHn+f9WCcelp3L/vci+LGbX8GigWaVphRArjVo5Pktkr9YnLy/mQ6VDkDyBD72dtfRNTgHmD2ts4rTDXKQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.977.7",
|
||||||
|
"@aws-sdk/nested-clients": "^3.997.42",
|
||||||
|
"@aws-sdk/token-providers": "3.1108.0",
|
||||||
|
"@aws-sdk/types": "^3.974.3",
|
||||||
|
"@smithy/core": "^3.31.1",
|
||||||
|
"@smithy/types": "^4.16.1",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/credential-provider-web-identity": {
|
||||||
|
"version": "3.972.74",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.74.tgz",
|
||||||
|
"integrity": "sha512-0YfczxGXF3RjGj8z7QG/Ho2HnLGKDHfPSHiTs47UU1U/+mmwISDN+rvGKt2zh+3FX8NdT4xd95LGBGyhQw2dgQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.977.7",
|
||||||
|
"@aws-sdk/nested-clients": "^3.997.42",
|
||||||
|
"@aws-sdk/types": "^3.974.3",
|
||||||
|
"@smithy/core": "^3.31.1",
|
||||||
|
"@smithy/types": "^4.16.1",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/middleware-sdk-s3": {
|
||||||
|
"version": "3.972.73",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.73.tgz",
|
||||||
|
"integrity": "sha512-oy7sRA5HvHcAvkcKX6F8RI240jcOf3c8y/Gqjs9qemIibdKQqGBIi0uwa+47ZRYqGLpdEO28TQU4G73yUzo06Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.977.7",
|
||||||
|
"@aws-sdk/signature-v4-multi-region": "^3.996.44",
|
||||||
|
"@aws-sdk/types": "^3.974.3",
|
||||||
|
"@smithy/core": "^3.31.1",
|
||||||
|
"@smithy/types": "^4.16.1",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/nested-clients": {
|
||||||
|
"version": "3.997.42",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.42.tgz",
|
||||||
|
"integrity": "sha512-XWRyon2MTHXD/zMoo0Mbge6Vwf+iE0qQaM/RyGO6NfZ9WukCFiQL27nQVZjYy2JwSIg+iXZxKOX95OBXqlSM4w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.977.7",
|
||||||
|
"@aws-sdk/signature-v4-multi-region": "^3.996.44",
|
||||||
|
"@aws-sdk/types": "^3.974.3",
|
||||||
|
"@smithy/core": "^3.31.1",
|
||||||
|
"@smithy/fetch-http-handler": "^5.6.13",
|
||||||
|
"@smithy/node-http-handler": "^4.9.13",
|
||||||
|
"@smithy/types": "^4.16.1",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/signature-v4-multi-region": {
|
||||||
|
"version": "3.996.44",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.44.tgz",
|
||||||
|
"integrity": "sha512-ZSfQ35Qn4MhSY+A0Whyr+KBx+wJKZUyBsOrjB2pSHOafRzbFe47T8XcXM8hZqUAC69qnqIy0C9ArxTuud0CC2w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/types": "^3.974.3",
|
||||||
|
"@smithy/signature-v4": "^5.6.12",
|
||||||
|
"@smithy/types": "^4.16.1",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/token-providers": {
|
||||||
|
"version": "3.1108.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1108.0.tgz",
|
||||||
|
"integrity": "sha512-rI80zxDxGJ6904eC/YbjkdjY6JdaZvQ01kOmrMvw7cFQGIHo27fhnIVbMSVDS4T6foQImjxYSRoOu/uSJscXDw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.977.7",
|
||||||
|
"@aws-sdk/nested-clients": "^3.997.42",
|
||||||
|
"@aws-sdk/types": "^3.974.3",
|
||||||
|
"@smithy/core": "^3.31.1",
|
||||||
|
"@smithy/types": "^4.16.1",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/types": {
|
||||||
|
"version": "3.974.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.3.tgz",
|
||||||
|
"integrity": "sha512-ECAqfpNsef+7MO8qtR0h9KcFIBAygaE7Cm6UOiQl+ft+uVap+1G7bNEjs4mdJE2OnA4m6k7i8peH8uGIAsOMGw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@smithy/types": "^4.16.1",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/xml-builder": {
|
||||||
|
"version": "3.972.38",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.38.tgz",
|
||||||
|
"integrity": "sha512-grf7mzfVxBS5AlsuTvBN7uDpzqohFww9fRPCO+EBSUdvtsYMcPSKdz54h/7XiscqNcUM1Ae1MF7JLHmiYYuzbQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@smithy/types": "^4.16.1",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws/lambda-invoke-store": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@pinecone-database/pinecone": {
|
||||||
|
"version": "6.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@pinecone-database/pinecone/-/pinecone-6.1.4.tgz",
|
||||||
|
"integrity": "sha512-wkipvpkBYNGYDeYj4azVVyCzSrekJE3Pgo0HImkbw80SGnTo9gz5kSbDCXCfVvFnhqC2q4nGikdTUvFCDiuruA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@smithy/core": {
|
||||||
|
"version": "3.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.32.0.tgz",
|
||||||
|
"integrity": "sha512-NAiCSC78fzbNIEWoheoF74Ob5ZorLijCHpMY26Fqvqg/+9LuyIqMfHDg2p8Yk1rqOyowtiL3y7WX0AW+teL6zw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@smithy/types": "^4.17.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@smithy/credential-provider-imds": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-2jsPi+7Zv2hSzD9IXR9D7DTqSn7mv4XalzRm+bESh53jiaUS3NKEUbpQFTJP0HhQy9qzZvluxQ3yS24zdRrqsA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@smithy/core": "^3.32.0",
|
||||||
|
"@smithy/types": "^4.17.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@smithy/fetch-http-handler": {
|
||||||
|
"version": "5.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.7.0.tgz",
|
||||||
|
"integrity": "sha512-W/exA8T0LEzCQtJ02w4IzaEQPIspgarqZprb7W8FwnYiDowgCrjl2fTQ6FvuSSUnJORuepBF81abmBJwqh+0XQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@smithy/core": "^3.32.0",
|
||||||
|
"@smithy/types": "^4.17.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@smithy/node-http-handler": {
|
||||||
|
"version": "4.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.10.0.tgz",
|
||||||
|
"integrity": "sha512-nrh7VxqzPQS/ip1hS293aI/OAWDWARQvjUxCfuKhyrfHa2gTdk28066RNeWLI1uuoHXaKAkOF8IcSAHuOp0+SA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@smithy/core": "^3.32.0",
|
||||||
|
"@smithy/types": "^4.17.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@smithy/signature-v4": {
|
||||||
|
"version": "5.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.0.tgz",
|
||||||
|
"integrity": "sha512-hCynhm22wMJ8wTF9crcwu8mxggtUrSLLJgDcGUvYFBqpofxycYJCGKOMYg4xtPPFtgNiDJSYmhsWLTrcU/g59Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@smithy/core": "^3.32.0",
|
||||||
|
"@smithy/types": "^4.17.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@smithy/types": {
|
||||||
|
"version": "4.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.17.0.tgz",
|
||||||
|
"integrity": "sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bowser": {
|
||||||
|
"version": "2.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
|
||||||
|
"integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/json-schema-to-ts": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.18.3",
|
||||||
|
"ts-algebra": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-algebra": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
package.json
Normal file
17
package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "rag-lambdas",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "node --test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.78.0",
|
||||||
|
"@aws-sdk/client-s3": "^3.864.0",
|
||||||
|
"@pinecone-database/pinecone": "^6.1.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
105
query.js
105
query.js
@@ -1,52 +1,89 @@
|
|||||||
import { BedrockRuntimeClient, InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime";
|
|
||||||
import { Pinecone } from "@pinecone-database/pinecone";
|
import { Pinecone } from "@pinecone-database/pinecone";
|
||||||
|
import { embedTexts, rerank } from "./lib/voyage.js";
|
||||||
|
import { generate } from "./lib/anthropic.js";
|
||||||
|
import { computeGroundedness } from "./lib/scoring.js";
|
||||||
|
|
||||||
const bedrock = new BedrockRuntimeClient({});
|
|
||||||
const pc = new Pinecone({ apiKey: process.env.PINECONE_API_KEY });
|
const pc = new Pinecone({ apiKey: process.env.PINECONE_API_KEY });
|
||||||
|
const TOP_K_SEARCH = 20;
|
||||||
|
const TOP_K_RERANK = 5;
|
||||||
|
|
||||||
export const handler = async (event) => {
|
export const handler = async (event) => {
|
||||||
const { question } = JSON.parse(event.body);
|
const { question } = JSON.parse(event.body || "{}");
|
||||||
|
if (!question || typeof question !== "string") {
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ error: "question is required" }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Embed user query
|
const [queryEmbedding] = await embedTexts([question], "query");
|
||||||
const embedResponse = await bedrock.send(new InvokeModelCommand({
|
|
||||||
modelId: "amazon.titan-embed-text-v1",
|
|
||||||
contentType: "application/json",
|
|
||||||
accept: "application/json",
|
|
||||||
body: JSON.stringify({ inputText: question })
|
|
||||||
}));
|
|
||||||
const { embedding } = JSON.parse(new TextDecoder().decode(embedResponse.body));
|
|
||||||
|
|
||||||
// 2. Query Vector DB for relevant context
|
|
||||||
const index = pc.index(process.env.PINECONE_INDEX_NAME);
|
const index = pc.index(process.env.PINECONE_INDEX_NAME);
|
||||||
const queryResponse = await index.query({
|
const queryResponse = await index.query({
|
||||||
vector: embedding,
|
vector: queryEmbedding,
|
||||||
topK: 3,
|
topK: TOP_K_SEARCH,
|
||||||
includeMetadata: true
|
includeMetadata: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const context = queryResponse.matches.map(match => match.metadata.text).join("\n\n");
|
const candidates = (queryResponse.matches || [])
|
||||||
|
.filter((m) => m.metadata && m.metadata.text)
|
||||||
const systemPrompt = `Use the following context to answer the question. If you don't know, say you don't know.\n\nContext:\n${context}`;
|
.map((m) => ({
|
||||||
|
id: m.id,
|
||||||
const llmResponse = await bedrock.send(new InvokeModelCommand({
|
text: m.metadata.text,
|
||||||
modelId: "anthropic.claude-3-haiku-20240307-v1:0", // Fast & Cost-effective
|
source: m.metadata.source || m.id,
|
||||||
contentType: "application/json",
|
chunkIndex: m.metadata.chunkIndex,
|
||||||
accept: "application/json",
|
|
||||||
body: JSON.stringify({
|
|
||||||
anthropic_version: "bedrock-2023-05-31",
|
|
||||||
max_tokens: 500,
|
|
||||||
messages: [
|
|
||||||
{ role: "user", content: `${systemPrompt}\n\nQuestion: ${question}` }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const result = JSON.parse(new TextDecoder().decode(llmResponse.body));
|
if (candidates.length === 0) {
|
||||||
const answer = result.content[0].text;
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
answer: "No sources found. Ingest documents first.",
|
||||||
|
citations: [],
|
||||||
|
groundednessScore: null,
|
||||||
|
followUpQuestions: [],
|
||||||
|
contextUsed: 0,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ranked = await rerank(question, candidates);
|
||||||
|
const topChunks = ranked.slice(0, TOP_K_RERANK).map((r) => r.document);
|
||||||
|
|
||||||
|
const sources = topChunks.map((chunk, i) => ({
|
||||||
|
docIndex: i + 1,
|
||||||
|
name: chunk.source,
|
||||||
|
text: chunk.text,
|
||||||
|
id: chunk.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { answer, citedSourceIndices, followUpQuestions } = await generate(question, sources);
|
||||||
|
|
||||||
|
const citedChunks = citedSourceIndices
|
||||||
|
.map((idx) => sources.find((s) => s.docIndex === idx))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const groundednessScore = await computeGroundedness(
|
||||||
|
answer,
|
||||||
|
citedChunks.map((c) => c.text)
|
||||||
|
);
|
||||||
|
|
||||||
|
const citations = citedChunks.map((c) => ({
|
||||||
|
sourceIndex: c.docIndex,
|
||||||
|
source: c.name,
|
||||||
|
chunkText: c.text,
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ answer, contextUsed: queryResponse.matches.length })
|
body: JSON.stringify({
|
||||||
|
answer,
|
||||||
|
citations,
|
||||||
|
groundednessScore,
|
||||||
|
followUpQuestions,
|
||||||
|
contextUsed: topChunks.length,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user