refactor services to use typescript
This commit is contained in:
260
server/src/services/documentService.ts
Normal file
260
server/src/services/documentService.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
'use strict';
|
||||
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import logger from '../logger.js';
|
||||
|
||||
const MODEL = 'claude-sonnet-4-5-20250929';
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
const MAX_SOURCE_CHARS = 30000;
|
||||
|
||||
export interface SourceChunk {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface SourceGroup {
|
||||
docIndex: number;
|
||||
name: string;
|
||||
chunks: SourceChunk[];
|
||||
}
|
||||
|
||||
function buildSourceBlock(sourceGroups: SourceGroup[]): string {
|
||||
const perSourceBudget = Math.floor(MAX_SOURCE_CHARS / (sourceGroups.length || 1));
|
||||
|
||||
return sourceGroups
|
||||
.map((g) => {
|
||||
const combined = g.chunks.map((c) => c.text).join('\n\n');
|
||||
const trimmed =
|
||||
combined.length > perSourceBudget
|
||||
? combined.slice(0, perSourceBudget) + '\n[...truncated]'
|
||||
: combined;
|
||||
return `[Source ${g.docIndex}] (${g.name})\n${trimmed}`;
|
||||
})
|
||||
.join('\n\n---\n\n');
|
||||
}
|
||||
|
||||
const STUDY_GUIDE_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'A concise title for the study guide' },
|
||||
sections: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
heading: { type: 'string' },
|
||||
bullets: { type: 'array', items: { type: 'string' }, description: 'Key points as bullet items' },
|
||||
keyTerms: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
term: { type: 'string' },
|
||||
definition: { type: 'string' },
|
||||
},
|
||||
required: ['term', 'definition'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
reviewQuestions: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Self-test questions for this section',
|
||||
},
|
||||
},
|
||||
required: ['heading', 'bullets', 'keyTerms', 'reviewQuestions'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
mnemonics: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Memory aids, simplified restatements, or mnemonic devices for the hardest concepts',
|
||||
},
|
||||
},
|
||||
required: ['title', 'sections', 'mnemonics'],
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
export interface KeyTerm {
|
||||
term: string;
|
||||
definition: string;
|
||||
}
|
||||
|
||||
export interface StudyGuideSection {
|
||||
heading: string;
|
||||
bullets: string[];
|
||||
keyTerms: KeyTerm[];
|
||||
reviewQuestions: string[];
|
||||
}
|
||||
|
||||
export interface StudyGuide {
|
||||
title: string;
|
||||
sections: StudyGuideSection[];
|
||||
mnemonics: string[];
|
||||
}
|
||||
|
||||
export async function generateStudyGuide(sourceGroups: SourceGroup[]): Promise<StudyGuide> {
|
||||
const client = getClient();
|
||||
const sources = buildSourceBlock(sourceGroups);
|
||||
const start = Date.now();
|
||||
|
||||
const prompt = `You are an expert educator. Given the source documents below, produce a comprehensive study guide. Follow these rules:
|
||||
|
||||
1. Create a structured outline of the main ideas organized into logical sections.
|
||||
2. For each section, provide: bullet-point key concepts, key terms with definitions, and 2-3 self-review questions.
|
||||
3. At the end, provide mnemonic devices or simplified restatements to help memorize the hardest concepts.
|
||||
4. Be concise but thorough. Use simple, clear language.
|
||||
|
||||
--- SOURCE DOCUMENTS ---
|
||||
|
||||
${sources}`;
|
||||
|
||||
const message = await client.messages.create({
|
||||
model: MODEL,
|
||||
max_tokens: 4096,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
output_config: { format: { type: 'json_schema', schema: STUDY_GUIDE_SCHEMA } },
|
||||
});
|
||||
|
||||
const textBlock = message.content[0];
|
||||
const raw = textBlock && 'text' in textBlock && textBlock.text ? textBlock.text : '{}';
|
||||
const parsed = JSON.parse(raw) as StudyGuide;
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
logger.info({ sections: parsed.sections?.length, elapsedSec: elapsed }, 'study guide generated');
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const FAQ_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
subject: { type: 'string', description: 'The identified common subject/theme across all sources' },
|
||||
faqPairs: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
question: { type: 'string' },
|
||||
answer: { type: 'string' },
|
||||
},
|
||||
required: ['question', 'answer'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
description: '8-15 frequently asked questions with concise answers',
|
||||
},
|
||||
},
|
||||
required: ['subject', 'faqPairs'],
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
export interface FaqPair {
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
export interface Faq {
|
||||
subject: string;
|
||||
faqPairs: FaqPair[];
|
||||
}
|
||||
|
||||
export async function generateFaq(sourceGroups: SourceGroup[]): Promise<Faq> {
|
||||
const client = getClient();
|
||||
const sources = buildSourceBlock(sourceGroups);
|
||||
const start = Date.now();
|
||||
|
||||
const prompt = `You are a subject-matter expert. Given the source documents below, perform two tasks:
|
||||
|
||||
1. IDENTIFY THE SUBJECT: Analyze all sources and determine the common subject, theme, or topic they share. Do NOT ask the user — infer it from the semantic overlap across the documents.
|
||||
2. GENERATE FAQ: Produce 8-15 frequently asked questions that someone studying or researching this subject would likely ask, along with concise, accurate answers grounded in the source material.
|
||||
|
||||
Make the questions range from foundational ("What is...") to more advanced/nuanced. Answers should be 2-4 sentences each.
|
||||
|
||||
--- SOURCE DOCUMENTS ---
|
||||
|
||||
${sources}`;
|
||||
|
||||
const message = await client.messages.create({
|
||||
model: MODEL,
|
||||
max_tokens: 4096,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
output_config: { format: { type: 'json_schema', schema: FAQ_SCHEMA } },
|
||||
});
|
||||
|
||||
const textBlock = message.content[0];
|
||||
const raw = textBlock && 'text' in textBlock && textBlock.text ? textBlock.text : '{}';
|
||||
const parsed = JSON.parse(raw) as Faq;
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
logger.info({ subject: parsed.subject, pairs: parsed.faqPairs?.length, elapsedSec: elapsed }, 'FAQ generated');
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const EXEC_BRIEF_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'The main subject header for the brief' },
|
||||
sections: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
subhead: { type: 'string' },
|
||||
prose: { type: 'string', description: 'Well-written prose paragraph(s) for this section' },
|
||||
},
|
||||
required: ['subhead', 'prose'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['title', 'sections'],
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
export interface ExecutiveBriefSection {
|
||||
subhead: string;
|
||||
prose: string;
|
||||
}
|
||||
|
||||
export interface ExecutiveBrief {
|
||||
title: string;
|
||||
sections: ExecutiveBriefSection[];
|
||||
}
|
||||
|
||||
export async function generateExecutiveBrief(sourceGroups: SourceGroup[]): Promise<ExecutiveBrief> {
|
||||
const client = getClient();
|
||||
const sources = buildSourceBlock(sourceGroups);
|
||||
const start = Date.now();
|
||||
|
||||
const prompt = `You are a senior analyst writing an executive brief. Given the source documents below, produce a highly organized, prose-form summary. Follow these rules:
|
||||
|
||||
1. Identify the overarching subject and use it as the main title/header.
|
||||
2. Organize the information into logical sections, each with a clear subheading.
|
||||
3. Write in polished prose — NO bullet points, NO lists. Use well-structured paragraphs.
|
||||
4. Condense the source material to the equivalent of approximately 3 pages. Prioritize the most important findings, arguments, and conclusions.
|
||||
5. Maintain an authoritative, professional tone suitable for executive-level readers.
|
||||
|
||||
--- SOURCE DOCUMENTS ---
|
||||
|
||||
${sources}`;
|
||||
|
||||
const message = await client.messages.create({
|
||||
model: MODEL,
|
||||
max_tokens: 4096,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
output_config: { format: { type: 'json_schema', schema: EXEC_BRIEF_SCHEMA } },
|
||||
});
|
||||
|
||||
const textBlock = message.content[0];
|
||||
const raw = textBlock && 'text' in textBlock && textBlock.text ? textBlock.text : '{}';
|
||||
const parsed = JSON.parse(raw) as ExecutiveBrief;
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
logger.info(
|
||||
{ title: parsed.title, sections: parsed.sections?.length, elapsedSec: elapsed },
|
||||
'executive brief generated'
|
||||
);
|
||||
return parsed;
|
||||
}
|
||||
Reference in New Issue
Block a user