refactor services to use typescript

This commit is contained in:
KS Jannette
2026-05-09 16:41:50 -04:00
parent c865e8f497
commit 1a1161b509
21 changed files with 970 additions and 531 deletions

View File

@@ -0,0 +1,216 @@
'use strict';
import Anthropic from '@anthropic-ai/sdk';
import logger from '../logger.js';
const MODEL = 'claude-opus-4-6';
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,
} as const;
export interface SourceChunk {
text: string;
}
export interface SourceGroup {
docIndex: number;
name: string;
chunks: SourceChunk[];
}
export interface GenerationResult {
answer: string;
citedSourceIndices: number[];
followUpQuestions: string[];
}
export interface CitationDetailParams {
chunkTexts: string[];
sourceName: string;
answer: string;
citationIndex: number;
}
export interface RelevantLink {
title: string;
url: string;
}
export interface CitationDetail {
citedSentence: string;
topicSummary: string;
additionalInsight: string;
relevantLinks: RelevantLink[];
}
function getClient(): Anthropic {
const key = process.env.ANTHROPIC_API_KEY;
if (!key) throw new Error('ANTHROPIC_API_KEY is not set');
return new Anthropic({ apiKey: key });
}
export function buildPrompt(query: string, sourceGroups: SourceGroup[]): string {
const today = new Date().toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
const sources = sourceGroups
.map((g) => {
const combined = g.chunks.map((c) => c.text).join('\n\n');
return `[Source ${g.docIndex}] (${g.name})\n${combined}`;
})
.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 ---
${sources}
--- USER QUESTION ---
${query}`;
}
export async function generate(query: string, sourceGroups: SourceGroup[]): Promise<GenerationResult> {
const client = getClient();
const message = await client.messages.create({
model: MODEL,
max_tokens: 2048,
messages: [{ role: 'user', content: buildPrompt(query, sourceGroups) }],
output_config: {
format: {
type: 'json_schema',
schema: RESPONSE_SCHEMA,
},
},
});
const textBlock = message.content[0];
const raw = textBlock && 'text' in textBlock ? textBlock.text : '';
logger.debug({ rawLength: raw.length }, 'claude response received');
const parsed = JSON.parse(raw) as {
answer?: string;
citedSourceIndices?: number[];
followUpQuestions?: string[];
};
const validIndices = new Set(sourceGroups.map((g) => g.docIndex));
const citedSourceIndices = [
...new Set((parsed.citedSourceIndices || []).filter((idx) => validIndices.has(idx))),
];
return {
answer: parsed.answer || '',
citedSourceIndices,
followUpQuestions: parsed.followUpQuestions || [],
};
}
const CITATION_DETAIL_SCHEMA = {
type: 'object',
properties: {
citedSentence: {
type: 'string',
description: 'The exact sentence or passage from the source material that the citation refers to',
},
topicSummary: {
type: 'string',
description: 'A concise summary of the broader topic/context surrounding the cited passage',
},
additionalInsight: {
type: 'string',
description:
'Additional expert insight, analysis, or context on the subject beyond what the source states',
},
relevantLinks: {
type: 'array',
items: {
type: 'object',
properties: {
title: { type: 'string', description: 'Descriptive link title' },
url: { type: 'string', description: 'Full URL to the resource' },
},
required: ['title', 'url'],
additionalProperties: false,
},
description: 'Up to 3 relevant web resources for further reading on the topic',
},
},
required: ['citedSentence', 'topicSummary', 'additionalInsight', 'relevantLinks'],
additionalProperties: false,
} as const;
export async function generateCitationDetail({
chunkTexts,
sourceName,
answer,
citationIndex,
}: CitationDetailParams): Promise<CitationDetail> {
const client = getClient();
const sourceText = chunkTexts.join('\n\n');
const prompt = `You are a research assistant. A user is reading a research response and wants to dive deeper into a specific citation. Below is the full answer they are reading, followed by the source material for citation [${citationIndex}] from "${sourceName}".
Your task:
1. Identify the specific sentence or passage from the source material that citation [${citationIndex}] refers to in the answer.
2. Summarize the broader topic/context surrounding that passage in the source material.
3. Provide additional expert insight or analysis on this subject that would help the reader understand it more deeply.
4. Suggest up to 3 relevant, real web resources (articles, papers, official sites) where the reader can learn more about this specific topic. Only include links you are highly confident are real and accessible.
--- ANSWER THE USER WAS READING ---
${answer}
--- SOURCE MATERIAL FOR [${citationIndex}] (${sourceName}) ---
${sourceText}`;
const message = await client.messages.create({
model: MODEL,
max_tokens: 1536,
messages: [{ role: 'user', content: prompt }],
output_config: {
format: {
type: 'json_schema',
schema: CITATION_DETAIL_SCHEMA,
},
},
});
const textBlock = message.content[0];
const raw = textBlock && 'text' in textBlock ? textBlock.text : '';
logger.debug({ citationIndex, rawLength: raw.length }, 'citation detail response received');
return JSON.parse(raw) as CitationDetail;
}