Skip to content
/ solutions / anthropic-rag-pipeline-for-sharepoint-knowledge-search Anthropic RAG Pipeline for SharePoint Knowledge Search Enable SMB teams to find answers across SharePoint document libraries using a hybrid RAG system with human escalation.
The problem Employees waste hours digging through scattered SharePoint documents, struggling with keyword search that misses relevant context and leaves questions unanswered.
Example artifact A complete, working implementation of this recipe — downloadable as a zip or browsable file by file. Generated by our build pipeline; tested with full coverage before publishing.
188 kB · 119 tests· 100.0% coverage· vitest passing
SHA-256 52eb09188818e9950df3af5e0a921edc6737d2d8fcf9836532e1330df4bed54e Comments Sign in to commentSign in with GitHub to comment and vote.
© 2026 REAA Technologies Inc. MIT-licensed open-source packages. No vendor lock-in. No bullshit.
On this page Intro
This recipe builds a hybrid RAG pipeline that indexes SharePoint document libraries and answers user questions using Claude. The system combines vector similarity and BM25 keyword search, manages your API budget, caches repeated queries, and escalates low-confidence answers to a Slack channel. You’ll wire up six REAA packages, a SharePoint Graph connector, and two Next.js API routes.
Prerequisites
Node.js 22+ and pnpm 10+
Azure AD app registration with https://graph.microsoft.com/.default permission
SharePoint site ID and drive/library ID
API keys: Anthropic, VoyageAI
Langfuse account (observability)
Slack incoming webhook URL
Step 1: Initialize the project scaffold
The scaffold agent already materialized a Next.js 16 App Router project with all configs in place. Your working directory already has the full structure — package.json, tsconfig.json, vitest.config.ts, next.config.ts, and the app/ and src/ directories. No need to run any create commands.
Verify the scaffold is intact by checking the package.json scripts:
cat package.json | grep -A 20 '"scripts"'
Expected output:
"scripts" : {
"dev" : "next dev" ,
"build" : "next build" ,
"start" : "next start" ,
"lint" : "eslint ." ,
"typecheck" : "tsc --noEmit" ,
"test" : "vitest run --coverage --reporter=json --outputFile=vitest-report.json"
}
The type field is "module" and engines.node is ">=22". These are locked by the scaffold and you should not change them.
Step 2: Configure environment variables
Copy .env.example to .env and fill in every variable. The file is in your project root:
Open .env in your editor and set each value:
NODE_ENV=development
# Anthropic
ANTHROPIC_API_KEY=your_anthropic_key_here
# VoyageAI
VOYAGE_API_KEY=your_voyage_api_key_here
# Azure AD — register an app at portal.azure.com, create a client secret
AZURE_TENANT_ID=your_tenant_id
AZURE_CLIENT_ID=your_client_id
AZURE_CLIENT_SECRET=your_client_secret
# SharePoint — find these via the SharePoint admin center or Graph API
SHAREPOINT_SITE_ID=your_site_id
SHAREPOINT_DRIVE_ID=your_drive_id
# Slack — create an incoming webhook at api.slack.com/apps
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/xxx/yyy/zzz
# Langfuse — sign up at cloud.langfuse.com
LANGFUSE_SECRET_KEY=sk-lf-xxx
LANGFUSE_PUBLIC_KEY=pk-lf-xxx
LANGFUSE_BASE_URL=https://cloud.langfuse.com
# LanceDB
LANCE_DB_DIR=./data/lancedb
Do not commit .env to version control — it already appears in .gitignore from the scaffold.
Step 3: Understand the type definitions
Before writing any feature code, examine the shared types at src/lib/types.ts. These define the interfaces that every module consumes:
import { z } from "zod/v4" ;
import type { Document } from "@reaatech/hybrid-rag" ;
export interface SharePointDocument extends Document {
id : string ;
content : string ;
source : string ;
metadata : {
The ChatRequestSchema uses Zod v4 (z.object, z.string().min(1)) and is the exact schema the API route validates against. The ChatResponse interface is what the /api/chat route returns.
Step 4: Build the SharePoint connector
The connector authenticates via Azure AD OAuth2 using @azure/identity and calls the Microsoft Graph API via @microsoft/microsoft-graph-client. Create src/lib/sharepoint/auth.ts:
import { ClientSecretCredential } from "@azure/identity" ;
export async function getAccessToken () : Promise < string > {
const tenantId = process.env.AZURE_TENANT_ID;
const clientId = process.env.AZURE_CLIENT_ID;
const clientSecret = process.env.AZURE_CLIENT_SECRET;
if ( ! tenantId || ! clientId || ! clientSecret) {
throw new Error (
"Missing Azure AD credentials. Set AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET environment variables."
Then create src/lib/sharepoint/connector.ts. The constructor accepts an optional TokenCredential or reads from environment variables as a fallback:
The syncAll() method is the entry point for the ingestion pipeline — it lists all documents in the drive and downloads each one, collecting per-file errors.
Step 5: Build the document ingestion pipeline
The ingestion pipeline chains SharePoint connector → document parsers → text chunker → embedder → LanceDB. Create the parsers first.
Create src/lib/ingestion/docx-parser.ts using mammoth:
import mammoth from "mammoth" ;
export async function parseDocx (buffer : Buffer ) : Promise <{ text : string ; metadata : Record < string , unknown > }> {
const result = await mammoth. extractRawText ({ buffer });
return { text: result.value, metadata: {} };
}
Create src/lib/ingestion/pdf-parser.ts using pdfjs-dist:
import { getDocument } from "pdfjs-dist" ;
export async function parsePdf (buffer : ArrayBuffer ) : Promise < string > {
const pdfDoc = await getDocument ({ data: buffer }).promise;
const texts : string [] = [];
for ( let i = 1 ; i <= pdfDoc.numPages; i ++ ) {
const page = await pdfDoc. getPage (i);
Create src/lib/ingestion/text-chunker.ts using ChunkingStrategy from @reaatech/hybrid-rag:
import { ChunkingStrategy, type ChunkingConfig, type Chunk } from "@reaatech/hybrid-rag" ;
export function chunkDocument (
text : string ,
config ?: Partial < ChunkingConfig >,
) : Chunk [] {
const {
strategy
Create src/lib/ingestion/voyage-embedder.ts:
import type { EmbeddingResult } from "@reaatech/hybrid-rag-embedding" ;
import { VoyageAIClient, VoyageAIError } from "voyageai" ;
export type { EmbeddingResult };
export class VoyageEmbedder {
private client : VoyageAIClient ;
private model : string ;
constructor ({ apiKey, model = "voyage-3-lite"
Create src/lib/ingestion/lancedb-store.ts:
import * as lancedb from "@lancedb/lancedb" ;
export class LanceDBStore {
private dbPath : string ;
private connection : lancedb . Connection | null = null ;
constructor ({ dbPath } : { dbPath ?:
Create src/lib/ingestion/pipeline.ts that wires everything together:
Step 6: Build the RAG query pipeline with Claude
The RAG pipeline handles cache lookup, budget enforcement, hybrid search, Claude generation, confidence routing, and Slack escalation. Create src/lib/anthropic/client.ts:
Create src/lib/anthropic/prompts.ts:
export interface Source {
content : string ;
source : string ;
}
export interface RagPrompt {
systemPrompt : string ;
userPrompt : string ;
}
export function buildRagPrompt (
query : string ,
sources :
Create src/lib/router/confidence.ts:
import { ConfidenceRouter } from "@reaatech/confidence-router" ;
export interface RoutingDecision {
type : "ROUTE" | "CLARIFY" | "FALLBACK" ;
target ?: string ;
prompt ?: string ;
}
export class AnswerRouter {
private router : ConfidenceRouter ;
constructor () {
const routeThreshold
Create src/lib/budget/controller.ts:
import { SpendStore } from "@reaatech/agent-budget-spend-tracker" ;
import { BudgetController } from "@reaatech/agent-budget-engine" ;
import { BudgetScope } from "@reaatech/agent-budget-types" ;
export interface BudgetControllerResult {
controller : BudgetController ;
store : SpendStore ;
}
export function createBudgetController (dailyLimitUsd : number ) : BudgetControllerResult {
const
Create src/lib/budget/interceptor.ts:
import { BudgetInterceptor } from "@reaatech/agent-budget-middleware" ;
import { BudgetController } from "@reaatech/agent-budget-engine" ;
import { BudgetScope, BudgetExceededError } from "@reaatech/agent-budget-types" ;
export
Create src/lib/cache/engine.ts:
import { CacheEngine, InMemoryAdapter } from "@reaatech/llm-cache" ;
import type { EmbeddingProvider } from
Create src/lib/rag/search.ts with the hybrid search (vector + BM25 + RRF):
Create src/lib/slack/alert.ts:
import type { SlackAlert } from "../types" ;
export interface SlackAlertResult {
ok : boolean ;
status : number ;
}
export async function sendSlackAlert (webhookUrl : string , alert : SlackAlert ) : Promise
Now create the main RAG pipeline at src/lib/rag/pipeline.ts. This is the core orchestrator:
Step 7: Wire up the Next.js API routes
Create app/api/chat/route.ts. This is the main entry point for user queries:
import { NextRequest, NextResponse } from "next/server" ;
import { ChatRequestSchema } from "@/src/lib/types" ;
import { traceRequest } from "@/src/lib/observability/trace" ;
import { BudgetExceededError } from "@reaatech/agent-budget-types" ;
import { ragPipeline } from "@/src/instrumentation" ;
Create app/api/ingest/route.ts for the SharePoint sync trigger:
import { NextRequest, NextResponse } from "next/server" ;
import type { IngestionResult } from "@/src/lib/types" ;
import { ingestionPipeline } from "@/src/instrumentation" ;
export async function POST (request : NextRequest ) {
try {
const body = await request. json () as { since ?: string };
if ( ! ingestionPipeline)
Step 8: Set up the instrumentation hook
Create src/instrumentation.ts to initialize all service singletons at startup. This is the file that experimental.instrumentationHook: true in next.config.ts activates:
Verify that next.config.ts has the instrumentationHook flag set:
import type { NextConfig } from "next" ;
const nextConfig = {
experimental: {
instrumentationHook: true ,
},
} as NextConfig ;
export default nextConfig;
If you ever add a new src/instrumentation.ts to a project and the hook isn’t firing, the first thing to check is whether this flag is present.
Step 9: Verify everything builds and tests pass
Run the full quality suite from the project root:
Expected output: no TypeScript errors. The config uses strict: true, noUncheckedIndexedAccess: true, and exactOptionalPropertyTypes: true, so every optional chain and indexed access must be handled.
Expected output: no ESLint errors. The config bans @ts-ignore, @ts-expect-error, eslint-disable, and : any across src/ and tests/.
Expected output: all tests pass with numFailedTests: 0. The coverage thresholds require lines >= 90, branches >= 90, functions >= 90, and statements >= 90 on the runtime code under src/ and app/**/route.ts. UI files like app/page.tsx are excluded from coverage.
node /home/rick/solutions-worker/bin/preflight.js
Expected output: {"ok": true, ...} with exit code 0. The preflight checker verifies banned patterns, exact-pinned versions, and scaffold integrity.
Next steps
Cron job for incremental sync — schedule POST /api/ingest with a { "since": "..." } body every hour so new and modified SharePoint documents get re-indexed automatically.
Streaming endpoint — expose a WebSocket route that calls ragPipeline.queryStream() so the frontend renders Claude’s response token by token instead of waiting for the full answer.
Multi-drive support — extend SharePointConnector to accept an array of drive IDs and fan out ingestion across multiple document libraries in parallel.
Eval harness — wire up Phoenix (arize.com/phoenix) to record every query, retrieved chunks, generated answer, and human rating so you can measure retrieval precision and answer quality over time.
Rate limiting — add middleware on the chat route to throttle requests per user using the X-Forwarded-For header so a single caller can’t exhaust the budget.
# Confidence routing thresholds
CONFIDENCE_ROUTE_THRESHOLD=0.8
CONFIDENCE_FALLBACK_THRESHOLD=0.3
# Budget guardrail
BUDGET_DAILY_LIMIT_USD=5.00
lastModified
:
string
;
driveId : string ;
itemId : string ;
};
title ?: string ;
author ?: string ;
date ?: string ;
}
export const ChatRequestSchema = z. object ({
query: z. string (). min ( 1 ),
useCase: z. string (). optional (),
temperature: z. number (). min ( 0 ). max ( 2 ). optional (),
maxTokens: z. number (). int (). positive (). optional (),
});
export type ChatRequest = z . infer < typeof ChatRequestSchema>;
export interface ChatResponse {
answer : string ;
sources : Array <{
chunkId : string ;
documentId : string ;
content : string ;
score : number ;
}>;
confidence : number ;
decision : "ROUTE" | "CLARIFY" | "FALLBACK" ;
cost : {
total : number ;
inputTokens : number ;
outputTokens : number ;
cacheSaved : number ;
};
}
export interface IngestionResult {
documentsProcessed : number ;
chunksCreated : number ;
embeddingCost : number ;
errors : Array <{ file : string ; error : string }>;
}
export interface SlackAlert {
query : string ;
confidence : number ;
topPredictions : Array <{ label : string ; confidence : number }>;
timestamp : string ;
}
);
}
const credential = new ClientSecretCredential (tenantId, clientId, clientSecret);
const tokenResponse = await credential. getToken ( "https://graph.microsoft.com/.default" );
return tokenResponse.token;
}
import { type TokenCredential, ClientSecretCredential } from "@azure/identity" ;
import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials/index" ;
import { Client } from "@microsoft/microsoft-graph-client" ;
interface GraphItem {
id : string ;
name ?: string ;
lastModifiedDateTime ?: string ;
[key : string ] : unknown ;
}
interface ListDocumentsResponse {
value ?: GraphItem [];
}
interface SyncAllResult {
downloaded : number ;
errors : Array <{ file : string ; error : string }>;
}
const EXTENSION_MIME_MAP : Record < string , string > = {
".docx" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ,
".pdf" : "application/pdf" ,
".txt" : "text/plain" ,
".md" : "text/markdown" ,
".csv" : "text/csv" ,
".xlsx" : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ,
".pptx" : "application/vnd.openxmlformats-officedocument.presentationml.presentation" ,
".html" : "text/html" ,
".json" : "application/json" ,
};
export class SharePointConnector {
private client : Client ;
private siteId : string ;
private driveId : string ;
constructor ({ siteId, driveId, credential } : { siteId : string ; driveId : string ; credential ?: TokenCredential }) {
const resolvedCredential = credential
?? (() => {
const tenantId = process.env.AZURE_TENANT_ID;
const clientId = process.env.AZURE_CLIENT_ID;
const clientSecret = process.env.AZURE_CLIENT_SECRET;
if ( ! tenantId || ! clientId || ! clientSecret) {
throw new Error (
"Missing Azure AD credentials. Set AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET environment variables."
);
}
return new ClientSecretCredential (tenantId, clientId, clientSecret);
})();
const authProvider = new TokenCredentialAuthenticationProvider (resolvedCredential, {
scopes: [ "https://graph.microsoft.com/.default" ],
});
this.client = Client. initWithMiddleware ({ authProvider });
this.siteId = siteId;
this.driveId = driveId;
}
async listDocuments (since ?: Date ) : Promise < GraphItem []> {
const response = ( await this.client
. api ( `/sites/${ this . siteId }/drives/${ this . driveId }/root/children` )
. get ()) as ListDocumentsResponse ;
const items = response.value ?? [];
if (since) {
return items. filter ((item) => {
if ( ! item.lastModifiedDateTime) return false ;
return new Date (item.lastModifiedDateTime) > since;
});
}
return items;
}
async downloadDocument (itemId : string , fileName ?: string ) : Promise <{ content : ArrayBuffer ; mimeType : string }> {
const response = ( await this.client
. api ( `/sites/${ this . siteId }/drives/${ this . driveId }/items/${ itemId }/content` )
. get ()) as ArrayBuffer ;
const mimeType = fileName
? (EXTENSION_MIME_MAP[fileName. slice (fileName. lastIndexOf ( "." )). toLowerCase ()] ?? "application/octet-stream" )
: "application/octet-stream" ;
return { content: response, mimeType };
}
async pollForChanges (since : Date ) : Promise < GraphItem []> {
return this. listDocuments (since);
}
async syncAll () : Promise < SyncAllResult > {
const items = await this. listDocuments ();
let downloaded = 0 ;
const errors : Array <{ file : string ; error : string }> = [];
for ( const item of items) {
try {
await this. downloadDocument (item.id, item.name);
downloaded ++ ;
} catch (err) {
const message = err instanceof Error ? err.message : String (err);
errors. push ({ file: item.name ?? item.id, error: message });
}
}
return { downloaded, errors };
}
} const textContent = await page. getTextContent ();
const items = textContent.items. filter ((item) => "str" in item);
const pageText = items. map ((item) => (item as { str : string }).str). join ( " " );
texts. push (pageText);
}
return texts. join ( "\n" );
}
=
ChunkingStrategy.FIXED_SIZE,
chunkSize = 512 ,
overlap = 50 ,
} = config ?? {};
if (strategy !== ChunkingStrategy.FIXED_SIZE) {
throw new Error ( `Unsupported chunking strategy: ${ strategy }` );
}
if ( ! text) {
return [
{
id: crypto. randomUUID (),
documentId: "" ,
index: 0 ,
content: "" ,
tokenCount: 0 ,
characterCount: 0 ,
startPosition: 0 ,
endPosition: 0 ,
metadata: {},
strategy: ChunkingStrategy.FIXED_SIZE,
},
];
}
const chunks : Chunk [] = [];
const step = chunkSize - overlap;
let position = 0 ;
for ( let i = 0 ; i < text.length; i += step) {
const end = Math. min (i + chunkSize, text.length);
const content = text. slice (i, end);
chunks. push ({
id: crypto. randomUUID (),
documentId: "" ,
index: position,
content,
tokenCount: Math. ceil (content.length / 4 ),
characterCount: content.length,
startPosition: i,
endPosition: end,
metadata: {},
strategy: ChunkingStrategy.FIXED_SIZE,
});
position ++ ;
if (end === text.length) break ;
}
return chunks;
}
}
:
{ apiKey
:
string
; model
?:
string
}) {
this.client = new VoyageAIClient ({ apiKey });
this.model = model;
}
async embed (texts : string []) : Promise < EmbeddingResult []> {
if (texts.length === 0 ) {
return [];
}
try {
const response = await this.client. embed ({ input: texts, model: this.model });
const data = response.data ?? [];
const usage = response.usage;
const totalTokens = usage?.totalTokens ?? 0 ;
const tokensPerItem = Math. ceil (totalTokens / data.length);
return data. map ((d) => ({
embedding: d.embedding ?? [],
tokens: tokensPerItem,
cost: 0 ,
}));
} catch (error) {
if (error instanceof VoyageAIError ) {
throw new Error ( `VoyageAI embedding failed: ${ error . message }` );
}
throw error;
}
}
}
string
}
=
{}) {
this.dbPath = dbPath ?? process.env.LANCE_DB_DIR ?? "./data/lancedb" ;
}
async connect () : Promise < lancedb . Connection > {
const conn = await lancedb. connect (this.dbPath);
this.connection = conn;
return conn;
}
async createTable (name : string , records : Record < string , unknown >[]) : Promise < void > {
const conn = this.connection ?? await this. connect ();
const table = await conn. createTable (name, records);
try {
await table. createIndex ( "vector" , {
config: lancedb.Index. ivfPq ({
numPartitions: 256 ,
numSubVectors: 64 ,
}),
});
} catch (err) {
console. warn ( "Failed to create LanceDB index, continuing without index:" , (err as Error ).message);
}
}
async search (
tableName : string ,
queryVector : number [],
topK : number ,
) : Promise < Record < string , unknown >[]> {
const conn = this.connection ?? await this. connect ();
const table = await conn. openTable (tableName);
const raw = await table. vectorSearch (queryVector). limit (topK). toArray ();
return raw as Record < string , unknown >[];
}
}
import type { IngestionResult } from "../types" ;
import type { Chunk } from "@reaatech/hybrid-rag" ;
import { chunkDocument } from "./text-chunker" ;
import { parseDocx } from "./docx-parser" ;
import { parsePdf } from "./pdf-parser" ;
import type { VoyageEmbedder, EmbeddingResult } from "./voyage-embedder" ;
import type { LanceDBStore } from "./lancedb-store" ;
interface GraphItem {
id : string ;
name ?: string ;
lastModifiedDateTime ?: string ;
[key : string ] : unknown ;
}
interface ISharePointConnector {
listDocuments (since ?: Date ) : Promise < GraphItem []>;
downloadDocument (itemId : string , fileName ?: string ) : Promise <{ content : ArrayBuffer ; mimeType : string }>;
pollForChanges (since : Date ) : Promise < GraphItem []>;
}
interface IngestionPipelineDeps {
sharepointConnector : ISharePointConnector ;
voyageEmbedder : VoyageEmbedder ;
lancedbStore : LanceDBStore ;
}
export class IngestionPipeline {
private sharepointConnector : ISharePointConnector ;
private voyageEmbedder : VoyageEmbedder ;
private lancedbStore : LanceDBStore ;
constructor (deps : IngestionPipelineDeps ) {
this.sharepointConnector = deps.sharepointConnector;
this.voyageEmbedder = deps.voyageEmbedder;
this.lancedbStore = deps.lancedbStore;
}
async run (options ?: { since ?: string }) : Promise < IngestionResult > {
await this.lancedbStore. connect ();
const items = options?.since
? await this.sharepointConnector. pollForChanges ( new Date (options.since))
: await this.sharepointConnector. listDocuments ();
let chunksCreated = 0 ;
let embeddingCost = 0 ;
const errors : Array <{ file : string ; error : string }> = [];
for ( const item of items) {
try {
const fileName = item.name ?? item.id;
const { content: buffer } = await this.sharepointConnector. downloadDocument (item.id, item.name);
const ext = fileName. split ( "." ). pop ()?. toLowerCase ();
let text : string ;
if (ext === "pdf" ) {
text = await parsePdf (buffer);
} else if (ext === "docx" ) {
const result = await parseDocx (Buffer. from (buffer));
text = result.text;
} else {
text = new TextDecoder (). decode (buffer);
}
const chunks = chunkDocument (text);
const texts = chunks. map ((chunk : Chunk ) => chunk.content);
const embeddings : EmbeddingResult [] = await this.voyageEmbedder. embed (texts);
const records = chunks. map ((chunk : Chunk , idx : number ) => ({
id: chunk.id,
documentId: chunk.documentId,
content: chunk.content,
position: chunk.index,
source: fileName,
embedding: embeddings[idx]?.embedding ?? [],
}));
await this.lancedbStore. createTable ( `doc_${ item . id }` , records);
chunksCreated += chunks.length;
embeddingCost += embeddings. reduce ((sum, e) => sum + e.cost, 0 );
} catch (error) {
errors. push ({
file: item.name ?? item.id,
error: error instanceof Error ? error.message : String (error),
});
}
}
return {
documentsProcessed: items.length,
chunksCreated,
embeddingCost,
errors,
};
}
async syncIncremental () : Promise < IngestionResult > {
return this. run ({ since: new Date (). toISOString () });
}
} import Anthropic from "@anthropic-ai/sdk" ;
import type {
ContentBlock,
TextBlock,
} from "@anthropic-ai/sdk/resources/messages/messages" ;
const SONNET_INPUT_PRICE = 0.000003 ;
const SONNET_OUTPUT_PRICE = 0.000015 ;
export interface GenerateAnswerParams {
systemPrompt : string ;
userQuery : string ;
context ?: string ;
maxTokens ?: number ;
temperature ?: number ;
}
export interface GenerateAnswerResult {
content : string ;
usage : {
inputTokens : number ;
outputTokens : number ;
};
cost : number ;
}
export type StreamDelta = { type : "delta" ; text : string };
export type StreamDone = {
type : "done" ;
usage : { inputTokens : number ; outputTokens : number };
cost : number ;
};
export type StreamEvent = StreamDelta | StreamDone ;
function computeCost (inputTokens : number , outputTokens : number ) : number {
return inputTokens * SONNET_INPUT_PRICE + outputTokens * SONNET_OUTPUT_PRICE;
}
export class AnthropicClient {
private client : Anthropic ;
private defaultModel : string ;
constructor ({
apiKey,
defaultModel = "claude-sonnet-4-6" ,
} : {
apiKey : string ;
defaultModel ?: string ;
}) {
this.client = new Anthropic ({ apiKey });
this.defaultModel = defaultModel;
}
async generateAnswer (
params : GenerateAnswerParams ,
) : Promise < GenerateAnswerResult > {
const {
systemPrompt,
userQuery,
context,
maxTokens = 1024 ,
temperature,
} = params;
const userContent = context
? `Context:\n${ context }\n\nQuestion: ${ userQuery }`
: userQuery;
const message = await this.client.messages. create ({
model: this.defaultModel,
max_tokens: maxTokens,
system: systemPrompt,
messages: [{ role: "user" , content: userContent }],
... (temperature !== undefined ? { temperature } : {}),
});
const content = message.content
. filter ((block : ContentBlock ) : block is TextBlock =>
block.type === "text" ,
)
. map ((block : TextBlock ) => block.text)
. join ( "\n" );
const inputTokens = message.usage.input_tokens;
const outputTokens = message.usage.output_tokens;
return {
content,
usage: { inputTokens, outputTokens },
cost: computeCost (inputTokens, outputTokens),
};
}
async * generateAnswerStream (
params : GenerateAnswerParams ,
) : AsyncGenerator < StreamEvent > {
const {
systemPrompt,
userQuery,
context,
maxTokens = 1024 ,
temperature,
} = params;
const userContent = context
? `Context:\n${ context }\n\nQuestion: ${ userQuery }`
: userQuery;
const stream = this.client.messages. stream ({
model: this.defaultModel,
max_tokens: maxTokens,
system: systemPrompt,
messages: [{ role: "user" , content: userContent }],
... (temperature !== undefined ? { temperature } : {}),
});
try {
for await ( const event of stream) {
if (event.type !== "content_block_delta" ) {
continue ;
}
const delta = event.delta;
if (delta.type !== "text_delta" ) {
continue ;
}
yield { type: "delta" , text: delta.text };
}
const finalMessage = await stream. finalMessage ();
const inputTokens = finalMessage.usage.input_tokens;
const outputTokens = finalMessage.usage.output_tokens;
yield {
type: "done" ,
usage: { inputTokens, outputTokens },
cost: computeCost (inputTokens, outputTokens),
};
} finally {
stream.controller. abort ();
}
}
} Array
<
Source
>,
) : RagPrompt {
const systemPrompt =
"You are a helpful assistant that answers questions based on the provided SharePoint document context. Use only the provided context to answer. If the context does not contain enough information, say so." ;
let userPrompt : string ;
if (sources.length === 0 ) {
userPrompt =
`Question: ${ query }\n\nNo SharePoint document context was provided for this query.` ;
} else {
const contextBlock = sources
. map (
(s, i) =>
`Source ${ String ( i + 1 ) } (${ s . source }):\n${ s . content }` ,
)
. join ( "\n\n" );
userPrompt = `Question: ${ query }\n\nSharePoint Document Context:\n${ contextBlock }` ;
}
return { systemPrompt, userPrompt };
}
=
Number
(process.env.CONFIDENCE_ROUTE_THRESHOLD
??
"0.8"
);
const fallbackThreshold = Number (process.env.CONFIDENCE_FALLBACK_THRESHOLD ?? "0.3" );
this.router = new ConfidenceRouter ({
routeThreshold,
fallbackThreshold,
clarificationEnabled: true ,
});
}
evaluate ({ predictions } : { predictions : Array <{ label : string ; confidence : number }> }) : RoutingDecision {
return this.router. decide ({ predictions });
}
}
store
=
new
SpendStore
();
const controller = new BudgetController ({ spendTracker: store });
controller. defineBudget ({
scopeType: BudgetScope.Org,
scopeKey: "sharepoint-rag" ,
limit: dailyLimitUsd,
policy: { softCap: 0.8 , hardCap: 1.0 , autoDowngrade: [], disableTools: [] },
});
return { controller, store };
}
interface
BudgetGuardScope
{
scopeType : BudgetScope ;
scopeKey : string ;
}
export interface CostRecord {
cost : number ;
inputTokens : number ;
outputTokens : number ;
requestId ?: string ;
}
export interface BudgetGuardAllowed {
allowed : true ;
result : CostRecord ;
}
export interface BudgetGuardDenied {
allowed : false ;
reason : string ;
}
export type BudgetGuardResult = BudgetGuardAllowed | BudgetGuardDenied ;
export class BudgetGuard {
private interceptor : BudgetInterceptor ;
constructor (deps : { controller : BudgetController }) {
this.interceptor = new BudgetInterceptor ({ controller: deps.controller });
}
checkAndRecord (
scope : BudgetGuardScope ,
modelId : string ,
estimatedCost : number ,
actualCostFn : () => Promise < CostRecord >,
) : Promise < BudgetGuardResult > {
try {
const ctx = this.interceptor. beforeStep ({
scope,
modelId,
estimatedCost,
tools: [],
});
if ( ! ctx.allowed) {
return Promise . resolve ({ allowed: false , reason: ctx.reason ?? "Budget exceeded" });
}
return actualCostFn (). then ((result) => {
this.interceptor. afterStep ({
scope,
allowed: true ,
modelId,
tools: [],
originalModelId: modelId,
originalTools: [],
actualCost: result.cost,
inputTokens: result.inputTokens,
outputTokens: result.outputTokens,
requestId: result.requestId ?? `req-${ String ( Date . now ()) }` ,
provider: "anthropic" ,
});
return { allowed: true , result };
});
} catch (error) {
if (error instanceof BudgetExceededError ) {
return Promise . resolve ({ allowed: false , reason: error.message });
}
throw error;
}
}
}
"@reaatech/llm-cache"
;
interface VoyageLikeEmbedder {
embed : (texts : string []) => Promise < Array <{ embedding : number []; tokens : number ; cost : number }>>;
}
export function createVoyageEmbeddingProvider (embedder : VoyageLikeEmbedder ) : EmbeddingProvider {
return {
async embed (text : string ) : Promise < number []> {
const results = await embedder. embed ([text]);
return results[ 0 ]?.embedding ?? [];
},
async embedBatch (texts : string []) : Promise < number [][]> {
const results = await embedder. embed (texts);
return results. map ((r) => r.embedding);
},
};
}
export function createCacheEngine (embedder : VoyageLikeEmbedder ) : CacheEngine {
return new CacheEngine ({
storage: new InMemoryAdapter (),
vectorStorage: new InMemoryAdapter (),
embedder: createVoyageEmbeddingProvider (embedder),
config: {
storage: { adapter: "memory" },
vectorStorage: { adapter: "memory" },
embedding: {
provider: "openai" as const ,
model: "voyage-3-lite" ,
dimensions: 512 ,
batchSize: 100 ,
maxRetries: 3 ,
},
similarity: { threshold: 0.85 , metric: "cosine" , maxResults: 5 },
ttl: {
default: 3600 ,
factual: 1800 ,
creative: 7200 ,
analytical: 3600 ,
sensitive: 600 ,
byUseCase: {},
},
segmentation: { enabled: true , defaultUseCase: "general" },
cost: { enabled: true , currency: "USD" },
observability: { metrics: true , tracing: false , logging: "info" },
},
});
}
export async function getCachedAnswer (
cache : CacheEngine ,
query : string ,
options ?: { useCase ?: string },
) {
return cache. get (query, {
model: "claude-sonnet-4-6" ,
modelVersion: "claude-sonnet-4-6-20250219" ,
useCase: options?.useCase ?? "general" ,
});
}
export async function cacheAnswer (
cache : CacheEngine ,
query : string ,
response : unknown ,
options ?: { useCase ?: string ; queryType ?: string },
) {
const metadata : Record < string , unknown > = {
model: "claude-sonnet-4-6" ,
modelVersion: "claude-sonnet-4-6-20250219" ,
useCase: options?.useCase ?? "general" ,
};
const cacheOptions : { queryType ?: "factual" | "creative" | "analytical" } = {};
if (options?.queryType !== undefined ) {
cacheOptions.queryType = options.queryType as "factual" | "creative" | "analytical" ;
}
return cache. set (query, response, metadata, cacheOptions);
}
import type { RetrievalResult } from "@reaatech/hybrid-rag" ;
interface VectorStore {
search (tableName : string , queryVector : number [], topK : number ) : Promise < Array < Record < string , unknown >>>;
}
interface Embedder {
embed : (texts : string []) => Promise < Array <{ embedding : number []; tokens : number ; cost : number }>>;
}
export class HybridSearchService {
private vectorStore : VectorStore ;
private embedder : Embedder ;
constructor (deps : { vectorStore : VectorStore ; embedder : Embedder }) {
this.vectorStore = deps.vectorStore;
this.embedder = deps.embedder;
}
async search (query : string , topK : number ) : Promise < RetrievalResult []> {
const embedResults = await this.embedder. embed ([query]);
const firstResult = embedResults[ 0 ];
if ( ! firstResult) {
return [];
}
const results = await this.vectorStore. search ( "documents" , firstResult.embedding, topK);
return results. map ((r) => ({
chunkId: (r.chunkId as string | undefined | null ) ?? (r.id as string ),
documentId: r.documentId as string ,
content: r.content as string ,
score: r.score as number ,
source: "vector" as const ,
metadata: (r.metadata as Record < string , unknown > | undefined ) ?? {},
}));
}
async hybridSearch (query : string , topK : number ) : Promise < RetrievalResult []> {
const vectorResults = await this. search (query, topK * 2 );
const bm25Results = this. bm25Rank (vectorResults, query);
return this. reciprocalRankFusion (vectorResults, bm25Results, topK);
}
private bm25Rank (
candidates : RetrievalResult [],
query : string ,
) : RetrievalResult [] {
const k1 = 1.2 ;
const b = 0.75 ;
const docs = candidates. map ((c) => c.content);
const avgDocLength = docs. reduce ((sum, d) => sum + d.length, 0 ) / Math. max (docs.length, 1 );
const queryTerms = query. toLowerCase (). split ( /\s + / );
const df : Record < string , number > = {};
for ( const doc of docs) {
const seen = new Set < string >();
for ( const term of doc. toLowerCase (). split ( /\s + / )) {
if ( ! seen. has (term)) {
df[term] = (df[term] ?? 0 ) + 1 ;
seen. add (term);
}
}
}
const N = docs.length;
return candidates. map ((c) => {
const docLen = c.content.length;
const terms = c.content. toLowerCase (). split ( /\s + / );
const termFreqs : Record < string , number > = {};
for ( const term of terms) {
termFreqs[term] = (termFreqs[term] ?? 0 ) + 1 ;
}
let score = 0 ;
for ( const term of queryTerms) {
const tf = termFreqs[term] ?? 0 ;
const n = df[term] ?? 0 ;
if (n === 0 ) continue ;
const idf = Math. log ( 1 + (N - n + 0.5 ) / (n + 0.5 ));
const numerator = tf * (k1 + 1 );
const denominator = tf + k1 * ( 1 - b + b * (docLen / avgDocLength));
score += idf * (numerator / denominator);
}
return {
chunkId: c.chunkId,
documentId: c.documentId,
content: c.content,
score,
source: "bm25" as const ,
metadata: c.metadata,
};
});
}
private reciprocalRankFusion (
vectorResults : RetrievalResult [],
bm25Results : RetrievalResult [],
topK : number ,
) : RetrievalResult [] {
const k = 60 ;
const scoreMap = new Map < string , number >();
const chunkMap = new Map < string , RetrievalResult >();
const addResults = (results : RetrievalResult [], rankWeight : number ) => {
for ( let i = 0 ; i < results.length; i ++ ) {
const r = results[i];
if ( ! r) continue ;
const score = rankWeight / (k + i + 1 );
scoreMap. set (r.chunkId, (scoreMap. get (r.chunkId) ?? 0 ) + score);
if ( ! chunkMap. has (r.chunkId)) {
chunkMap. set (r.chunkId, r);
}
}
};
addResults (vectorResults, 1 );
addResults (bm25Results, 1 );
const fused = Array. from (scoreMap. entries ())
. map (([chunkId, score]) => {
const original = chunkMap. get (chunkId);
if ( ! original) return null ;
return {
chunkId: original.chunkId,
documentId: original.documentId,
content: original.content,
score,
source: original.source,
metadata: original.metadata,
} satisfies RetrievalResult ;
})
. filter ((item) : item is RetrievalResult => item !== null )
. sort ((a, b) => b.score - a.score)
. slice ( 0 , topK);
return fused;
}
} <
SlackAlertResult
> {
const body = {
blocks: [
{
type: "header" ,
text: { type: "plain_text" , text: `Query: ${ alert . query }` , emoji: true },
},
{
type: "context" ,
elements: [{ type: "mrkdwn" , text: `*Confidence:* ${ String ( alert . confidence ) }` }],
},
{ type: "divider" },
{
type: "section" ,
text: {
type: "mrkdwn" ,
text: alert.topPredictions
. map ((p) => `• *${ p . label }:* ${ String ( p . confidence ) }` )
. join ( "\n" ),
},
},
{
type: "actions" ,
elements: [
{
type: "button" ,
text: { type: "plain_text" , text: "Investigate" , emoji: true },
value: "investigate" ,
},
],
},
],
};
try {
const response = await fetch (webhookUrl, {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON. stringify (body),
});
return { ok: response.ok, status: response.status };
} catch {
return { ok: false , status: 0 };
}
}
import type { ChatResponse } from "../types" ;
import type { CacheEngine, CacheResult } from "@reaatech/llm-cache" ;
import { BudgetScope } from "@reaatech/agent-budget-types" ;
import type { BudgetGuardScope, CostRecord, BudgetGuardResult } from "../budget/interceptor" ;
import type { RoutingDecision } from "../router/confidence" ;
import type { GenerateAnswerResult } from "../anthropic/client" ;
import type { Source } from "../anthropic/prompts" ;
import { HybridSearchService } from "./search" ;
import { AnthropicClient } from "../anthropic/client" ;
import { buildRagPrompt } from "../anthropic/prompts" ;
import { AnswerRouter } from "../router/confidence" ;
import { BudgetGuard } from "../budget/interceptor" ;
import { sendSlackAlert } from "../slack/alert" ;
import { getCachedAnswer, cacheAnswer } from "../cache/engine" ;
function toSources (
items : Array <{ chunkId : string ; documentId : string ; content : string ; score : number }>,
) : Array < Source > {
return items. map ((item) => ({
content: item.content,
source: item.documentId,
}));
}
export class RagPipeline {
private searchService : HybridSearchService ;
private anthropicClient : AnthropicClient ;
private answerRouter : AnswerRouter ;
private budgetGuard : BudgetGuard ;
private cache : CacheEngine ;
private slackWebhookUrl : string ;
private scope : BudgetGuardScope = {
scopeType: BudgetScope.Org,
scopeKey: "sharepoint-rag" ,
};
constructor (deps : {
searchService : HybridSearchService ;
anthropicClient : AnthropicClient ;
answerRouter : AnswerRouter ;
budgetGuard : BudgetGuard ;
cache : CacheEngine ;
slackWebhookUrl : string ;
}) {
this.searchService = deps.searchService;
this.anthropicClient = deps.anthropicClient;
this.answerRouter = deps.answerRouter;
this.budgetGuard = deps.budgetGuard;
this.cache = deps.cache;
this.slackWebhookUrl = deps.slackWebhookUrl;
}
async query (params : {
query : string ;
useCase ?: string ;
temperature ?: number ;
maxTokens ?: number ;
}) : Promise < ChatResponse > {
const { query, useCase, temperature, maxTokens } = params;
const cacheOptions = useCase !== undefined ? { useCase } : {};
const cacheResult : CacheResult = await getCachedAnswer (this.cache, query, cacheOptions);
if (cacheResult.hit) {
const cached = cacheResult.entry.response as ChatResponse ;
return {
... cached,
cost: { ... cached.cost, cacheSaved: cached.cost.total },
};
}
const sources = await this.searchService. hybridSearch (query, 5 );
const { systemPrompt, userPrompt } = buildRagPrompt (query, toSources (sources));
let generatedAnswer = "" ;
let inputTokens = 0 ;
let outputTokens = 0 ;
let totalCost = 0 ;
const budgetResult = await this.budgetGuard. checkAndRecord (
this.scope,
"claude-sonnet-4-6" ,
0.05 ,
async () : Promise < CostRecord > => {
const generateParams : { systemPrompt : string ; userQuery : string ; maxTokens ?: number ; temperature ?: number } = {
systemPrompt,
userQuery: userPrompt,
};
if (maxTokens !== undefined ) generateParams.maxTokens = maxTokens;
if (temperature !== undefined ) generateParams.temperature = temperature;
const result : GenerateAnswerResult = await this.anthropicClient. generateAnswer (generateParams);
generatedAnswer = result.content;
inputTokens = result.usage.inputTokens;
outputTokens = result.usage.outputTokens;
totalCost = result.cost;
return {
cost: result.cost,
inputTokens: result.usage.inputTokens,
outputTokens: result.usage.outputTokens,
};
},
);
if ( ! budgetResult.allowed) {
await sendSlackAlert (this.slackWebhookUrl, {
query,
confidence: 0 ,
topPredictions: [{ label: "rag_answer" , confidence: 0 }],
timestamp: new Date (). toISOString (),
});
return {
answer: "Budget exceeded" ,
sources: [],
confidence: 0 ,
decision: "FALLBACK" ,
cost: { total: 0 , inputTokens: 0 , outputTokens: 0 , cacheSaved: 0 },
};
}
const confidence = this. deriveConfidence (generatedAnswer);
const decision : RoutingDecision = this.answerRouter. evaluate ({
predictions: [{ label: "rag_answer" , confidence }],
});
if (decision.type === "FALLBACK" ) {
await sendSlackAlert (this.slackWebhookUrl, {
query,
confidence,
topPredictions: [{ label: "rag_answer" , confidence }],
timestamp: new Date (). toISOString (),
});
return {
answer: "I'm unable to provide a confident answer to this question. A specialist has been notified." ,
sources: sources. map ((s) => ({
chunkId: s.chunkId,
documentId: s.documentId,
content: s.content,
score: s.score,
})),
confidence,
decision: "FALLBACK" ,
cost: { total: totalCost, inputTokens, outputTokens, cacheSaved: 0 },
};
}
if (decision.type === "CLARIFY" ) {
return {
answer: decision.prompt ?? "Could you please provide more details?" ,
sources: sources. map ((s) => ({
chunkId: s.chunkId,
documentId: s.documentId,
content: s.content,
score: s.score,
})),
confidence,
decision: "CLARIFY" ,
cost: { total: totalCost, inputTokens, outputTokens, cacheSaved: 0 },
};
}
const response : ChatResponse = {
answer: generatedAnswer,
sources: sources. map ((s) => ({
chunkId: s.chunkId,
documentId: s.documentId,
content: s.content,
score: s.score,
})),
confidence,
decision: "ROUTE" ,
cost: { total: totalCost, inputTokens, outputTokens, cacheSaved: 0 },
};
const queryCacheOptions = useCase !== undefined ? { useCase } : {};
await cacheAnswer (this.cache, query, response, queryCacheOptions);
return response;
}
async * queryStream (
params : {
query : string ;
useCase ?: string ;
temperature ?: number ;
maxTokens ?: number ;
},
) : AsyncGenerator <
{ type : "delta" ; content : string } | { type : "done" ; response : ChatResponse }
> {
const { query, useCase, temperature, maxTokens } = params;
const streamCacheOptions = useCase !== undefined ? { useCase } : {};
const cacheResult : CacheResult = await getCachedAnswer (this.cache, query, streamCacheOptions);
if (cacheResult.hit) {
const cached = cacheResult.entry.response as ChatResponse ;
yield {
type: "done" ,
response: {
... cached,
cost: { ... cached.cost, cacheSaved: cached.cost.total },
},
};
return ;
}
const sources = await this.searchService. hybridSearch (query, 5 );
const { systemPrompt, userPrompt } = buildRagPrompt (query, toSources (sources));
const deltas : string [] = [];
let fullAnswer = "" ;
let inputTokens = 0 ;
let outputTokens = 0 ;
let totalCost = 0 ;
const streamResult : BudgetGuardResult = await this.budgetGuard. checkAndRecord (
this.scope,
"claude-sonnet-4-6" ,
0.05 ,
async () : Promise < CostRecord > => {
const generateParams : { systemPrompt : string ; userQuery : string ; maxTokens ?: number ; temperature ?: number } = {
systemPrompt,
userQuery: userPrompt,
};
if (maxTokens !== undefined ) generateParams.maxTokens = maxTokens;
if (temperature !== undefined ) generateParams.temperature = temperature;
const stream = this.anthropicClient. generateAnswerStream (generateParams);
for await ( const event of stream) {
if (event.type === "delta" ) {
fullAnswer += event.text;
deltas. push (event.text);
} else {
inputTokens = event.usage.inputTokens;
outputTokens = event.usage.outputTokens;
totalCost = event.cost;
}
}
return {
cost: totalCost,
inputTokens,
outputTokens,
};
},
);
for ( const delta of deltas) {
yield { type: "delta" , content: delta };
}
if ( ! streamResult.allowed) {
await sendSlackAlert (this.slackWebhookUrl, {
query,
confidence: 0 ,
topPredictions: [{ label: "rag_answer" , confidence: 0 }],
timestamp: new Date (). toISOString (),
});
yield {
type: "done" ,
response: {
answer: "Budget exceeded" ,
sources: [],
confidence: 0 ,
decision: "FALLBACK" ,
cost: { total: 0 , inputTokens: 0 , outputTokens: 0 , cacheSaved: 0 },
},
};
return ;
}
const actualCost = streamResult.result.cost;
const confidence = this. deriveConfidence (fullAnswer);
const decision : RoutingDecision = this.answerRouter. evaluate ({
predictions: [{ label: "rag_answer" , confidence }],
});
if (decision.type === "FALLBACK" ) {
await sendSlackAlert (this.slackWebhookUrl, {
query,
confidence,
topPredictions: [{ label: "rag_answer" , confidence }],
timestamp: new Date (). toISOString (),
});
yield {
type: "done" ,
response: {
answer: "I'm unable to provide a confident answer to this question. A specialist has been notified." ,
sources: sources. map ((s) => ({
chunkId: s.chunkId,
documentId: s.documentId,
content: s.content,
score: s.score,
})),
confidence,
decision: "FALLBACK" ,
cost: { total: actualCost, inputTokens, outputTokens, cacheSaved: 0 },
},
};
return ;
}
if (decision.type === "CLARIFY" ) {
yield {
type: "done" ,
response: {
answer: decision.prompt ?? "Could you please provide more details?" ,
sources: sources. map ((s) => ({
chunkId: s.chunkId,
documentId: s.documentId,
content: s.content,
score: s.score,
})),
confidence,
decision: "CLARIFY" ,
cost: { total: actualCost, inputTokens, outputTokens, cacheSaved: 0 },
},
};
return ;
}
const response : ChatResponse = {
answer: fullAnswer,
sources: sources. map ((s) => ({
chunkId: s.chunkId,
documentId: s.documentId,
content: s.content,
score: s.score,
})),
confidence,
decision: "ROUTE" ,
cost: { total: actualCost, inputTokens, outputTokens, cacheSaved: 0 },
};
const streamQueryCacheOptions = useCase !== undefined ? { useCase } : {};
await cacheAnswer (this.cache, query, response, streamQueryCacheOptions);
yield { type: "done" , response };
}
private deriveConfidence (answer : string ) : number {
if (answer.length > 100 && /[A-Z]/ . test (answer) && answer. includes ( "." )) {
return 0.9 ;
}
return 0.2 ;
}
} export async function POST (request : NextRequest ) {
try {
const body : unknown = await request. json ();
const parsed = ChatRequestSchema. safeParse (body);
if ( ! parsed.success) {
return NextResponse. json (
{ error: "Validation failed" , details: parsed.error.issues },
{ status: 400 },
);
}
const { query, useCase, temperature, maxTokens } = parsed.data;
const result = await traceRequest ( async () => {
if ( ! ragPipeline) throw new Error ( "RAG pipeline not initialized" );
return ragPipeline. query ({
query,
... (useCase !== undefined ? { useCase } : {}),
... (temperature !== undefined ? { temperature } : {}),
... (maxTokens !== undefined ? { maxTokens } : {}),
});
});
return NextResponse. json (result);
} catch (error) {
if (error instanceof BudgetExceededError ) {
return NextResponse. json (
{ error: "Budget exceeded" , message: error.message },
{ status: 402 },
);
}
return NextResponse. json (
{ error: "Internal server error" },
{ status: 500 },
);
}
}
export function GET () {
return NextResponse. json ({
status: "ok" ,
service: "sharepoint-rag-chat" ,
});
}
throw
new
Error
(
"Ingestion pipeline not initialized"
);
const runOptions : { since ?: string } = {};
if (body.since !== undefined ) runOptions.since = body.since;
const result : IngestionResult = await ingestionPipeline. run (runOptions);
return NextResponse. json (result);
} catch {
return NextResponse. json (
{ error: "Internal server error" },
{ status: 500 },
);
}
}
import { initLangfuse } from "@/src/lib/observability/langfuse" ;
import { VoyageEmbedder } from "@/src/lib/ingestion/voyage-embedder" ;
import { LanceDBStore } from "@/src/lib/ingestion/lancedb-store" ;
import { AnthropicClient } from "@/src/lib/anthropic/client" ;
import { SharePointConnector } from "@/src/lib/sharepoint/connector" ;
import { BudgetGuard } from "@/src/lib/budget/interceptor" ;
import { AnswerRouter } from "@/src/lib/router/confidence" ;
import { createCacheEngine } from "@/src/lib/cache/engine" ;
import { RagPipeline } from "@/src/lib/rag/pipeline" ;
import { IngestionPipeline } from "@/src/lib/ingestion/pipeline" ;
import { createBudgetController } from "@/src/lib/budget/controller" ;
import { HybridSearchService } from "@/src/lib/rag/search" ;
import type { CacheEngine } from "@reaatech/llm-cache" ;
import type { BudgetController } from "@reaatech/agent-budget-engine" ;
export let voyageEmbedder : VoyageEmbedder | null = null ;
export let lanceDBStore : LanceDBStore | null = null ;
export let anthropicClient : AnthropicClient | null = null ;
export let sharePointConnector : SharePointConnector | null = null ;
export let budgetGuard : BudgetGuard | null = null ;
export let budgetController : BudgetController | null = null ;
export let answerRouter : AnswerRouter | null = null ;
export let cacheEngine : CacheEngine | null = null ;
export let ragPipeline : RagPipeline | null = null ;
export let ingestionPipeline : IngestionPipeline | null = null ;
export async function register () : Promise < void > {
if (process.env.NEXT_RUNTIME !== "nodejs" ) return ;
await Promise . resolve ();
try {
initLangfuse ();
} catch {
}
try {
voyageEmbedder = new VoyageEmbedder ({ apiKey: process.env.VOYAGE_API_KEY ?? "" });
} catch {
}
try {
lanceDBStore = new LanceDBStore ({ dbPath: process.env.LANCE_DB_DIR ?? "./data/lancedb" });
} catch {
}
try {
anthropicClient = new AnthropicClient ({ apiKey: process.env.ANTHROPIC_API_KEY ?? "" });
} catch {
}
if (process.env.SHAREPOINT_SITE_ID && process.env.SHAREPOINT_DRIVE_ID) {
try {
sharePointConnector = new SharePointConnector ({
siteId: process.env.SHAREPOINT_SITE_ID,
driveId: process.env.SHAREPOINT_DRIVE_ID,
});
} catch {
}
}
try {
const { controller } = createBudgetController (
Number (process.env.BUDGET_DAILY_LIMIT_USD ?? "5" ),
);
budgetController = controller;
budgetGuard = new BudgetGuard ({ controller });
} catch {
}
try {
answerRouter = new AnswerRouter ();
} catch {
}
if (voyageEmbedder) {
try {
cacheEngine = createCacheEngine (voyageEmbedder);
} catch {
}
}
if (voyageEmbedder && lanceDBStore && anthropicClient && answerRouter && budgetGuard && cacheEngine) {
try {
const searchService = new HybridSearchService ({
vectorStore: lanceDBStore,
embedder: voyageEmbedder,
});
ragPipeline = new RagPipeline ({
searchService,
anthropicClient,
answerRouter,
budgetGuard,
cache: cacheEngine,
slackWebhookUrl: process.env.SLACK_WEBHOOK_URL ?? "" ,
});
} catch {
}
}
if (sharePointConnector && voyageEmbedder && lanceDBStore) {
try {
ingestionPipeline = new IngestionPipeline ({
sharepointConnector: sharePointConnector,
voyageEmbedder,
lancedbStore: lanceDBStore,
});
} catch {
}
}
}