Merge pull request #1 from kjannette/ts-refactor-two

addiitonal ts implementation
This commit is contained in:
S Jannette
2026-05-09 17:18:32 -04:00
committed by GitHub
4 changed files with 63 additions and 41 deletions

View File

@@ -3,12 +3,13 @@
import { Router, type Request, type Response, type NextFunction } from 'express'; import { Router, type Request, type Response, type NextFunction } from 'express';
import logger from '../logger.js'; import logger from '../logger.js';
import * as notebookStore from '../stores/notebookStore.js'; import * as notebookStore from '../stores/notebookStore.js';
import type { TextChunk, Source, SourceGroup } from '../stores/notebookStore.js';
import * as documentCacheStore from '../stores/documentCacheStore.js'; import * as documentCacheStore from '../stores/documentCacheStore.js';
import type { CacheEntry } from '../stores/documentCacheStore.js';
import { import {
generateStudyGuide, generateStudyGuide,
generateFaq, generateFaq,
generateExecutiveBrief, generateExecutiveBrief,
type SourceGroup,
type StudyGuide, type StudyGuide,
type Faq, type Faq,
type ExecutiveBrief, type ExecutiveBrief,
@@ -47,28 +48,28 @@ router.post('/generate', async (req: Request<unknown, unknown, GenerateBody>, re
return; return;
} }
const chunks = notebookStore.getChunksForNotebook(notebookId); const chunks: TextChunk[] = notebookStore.getChunksForNotebook(notebookId);
if (chunks.length === 0) { if (chunks.length === 0) {
res.status(422).json({ error: 'No source material available in this notebook' }); res.status(422).json({ error: 'No source material available in this notebook' });
return; return;
} }
const sources = notebookStore.getSources(notebookId); const sources: Source[] = notebookStore.getSources(notebookId);
const cached = documentCacheStore.getCachedDocument(notebookId, type); const cached: CacheEntry | null = documentCacheStore.getCachedDocument(notebookId, type);
if (cached && documentCacheStore.isFresh(notebookId, type, sources)) { if (cached && documentCacheStore.isFresh(notebookId, type, sources)) {
logger.info({ notebookId, type }, 'serving cached document'); logger.info({ notebookId, type }, 'serving cached document');
res.json({ type, document: cached.document }); res.json({ type, document: cached.document });
return; return;
} }
const sourceGroups = notebookStore.buildSourceGroups(notebookId, chunks); const sourceGroups: SourceGroup[] = notebookStore.buildSourceGroups(notebookId, chunks);
logger.info( logger.info(
{ notebookId, type, sourceCount: sourceGroups.length, chunkCount: chunks.length }, { notebookId, type, sourceCount: sourceGroups.length, chunkCount: chunks.length },
'document generation started' 'document generation started'
); );
const document = await generator(sourceGroups); const document: StudyGuide | Faq | ExecutiveBrief = await generator(sourceGroups);
documentCacheStore.setCachedDocument(notebookId, type, document, sources); documentCacheStore.setCachedDocument(notebookId, type, document, sources);
logger.info({ notebookId, type }, 'document generation complete'); logger.info({ notebookId, type }, 'document generation complete');

View File

@@ -3,9 +3,12 @@
import { Router, type Request, type Response, type NextFunction } from 'express'; import { Router, type Request, type Response, type NextFunction } from 'express';
import logger from '../logger.js'; import logger from '../logger.js';
import { embedTexts, search, rerank } from '../services/retrievalService.js'; import { embedTexts, search, rerank } from '../services/retrievalService.js';
import type { ScoredChunk, RankedChunk, TextChunk } from '../services/retrievalService.js';
import { generate } from '../services/generationService.js'; import { generate } from '../services/generationService.js';
import type { GenerationResult } from '../services/generationService.js';
import { computeGroundedness } from '../services/scoringService.js'; import { computeGroundedness } from '../services/scoringService.js';
import * as notebookStore from '../stores/notebookStore.js'; import * as notebookStore from '../stores/notebookStore.js';
import type { SourceGroup } from '../stores/notebookStore.js';
const TOP_K_SEARCH = 20; const TOP_K_SEARCH = 20;
const TOP_K_RERANK = 5; const TOP_K_RERANK = 5;
@@ -17,6 +20,20 @@ interface QueryBody {
question?: string; question?: string;
} }
interface Citation {
sourceIndex: number;
sourceId: string | null;
name: string | null;
chunkTexts: string[];
}
interface QueryResponse {
answer: string;
citations: Citation[];
groundednessScore: number | null;
followUpQuestions: string[];
}
router.post('/', async (req: Request<unknown, unknown, QueryBody>, res: Response, next: NextFunction) => { router.post('/', async (req: Request<unknown, unknown, QueryBody>, res: Response, next: NextFunction) => {
try { try {
const { notebookId, question } = req.body; const { notebookId, question } = req.body;
@@ -27,9 +44,9 @@ router.post('/', async (req: Request<unknown, unknown, QueryBody>, res: Response
logger.info({ notebookId, question }, 'query received'); logger.info({ notebookId, question }, 'query received');
const [queryEmbedding] = await embedTexts([question], 'query'); const [queryEmbedding]: number[][] = await embedTexts([question], 'query');
const searchResults = search(queryEmbedding, notebookId, TOP_K_SEARCH); const searchResults: ScoredChunk[] = search(queryEmbedding, notebookId, TOP_K_SEARCH);
if (searchResults.length === 0) { if (searchResults.length === 0) {
res.json({ res.json({
answer: 'No sources found for this notebook. Upload some documents first.', answer: 'No sources found for this notebook. Upload some documents first.',
@@ -40,9 +57,9 @@ router.post('/', async (req: Request<unknown, unknown, QueryBody>, res: Response
return; return;
} }
const candidateChunks = searchResults.map((r) => r.chunk); const candidateChunks: TextChunk[] = searchResults.map((r) => r.chunk);
const reranked = await rerank(question, candidateChunks); const reranked: RankedChunk[] = await rerank(question, candidateChunks);
const topChunks = reranked.slice(0, TOP_K_RERANK).map((r) => r.chunk); const topChunks: TextChunk[] = reranked.slice(0, TOP_K_RERANK).map((r) => r.chunk);
logger.debug( logger.debug(
{ {
@@ -52,24 +69,24 @@ router.post('/', async (req: Request<unknown, unknown, QueryBody>, res: Response
'retrieval complete' 'retrieval complete'
); );
const sourceGroups = notebookStore.buildSourceGroups(notebookId, topChunks); const sourceGroups: SourceGroup[] = notebookStore.buildSourceGroups(notebookId, topChunks);
const { answer, citedSourceIndices, followUpQuestions } = await generate(question, sourceGroups); const { answer, citedSourceIndices, followUpQuestions }: GenerationResult = await generate(question, sourceGroups);
const citedChunkIds: string[] = []; const citedChunkIds: string[] = [];
for (const idx of citedSourceIndices) { for (const idx of citedSourceIndices) {
const group = sourceGroups.find((g) => g.docIndex === idx); const group: SourceGroup | undefined = sourceGroups.find((g) => g.docIndex === idx);
if (group) { if (group) {
citedChunkIds.push(...group.chunks.map((c) => c.id)); citedChunkIds.push(...group.chunks.map((c) => c.id));
} }
} }
const groundednessScore = await computeGroundedness(answer, citedChunkIds); const groundednessScore: number = await computeGroundedness(answer, citedChunkIds);
logger.info({ groundednessScore: groundednessScore.toFixed(3) }, 'query complete'); logger.info({ groundednessScore: groundednessScore.toFixed(3) }, 'query complete');
const citations = citedSourceIndices.map((idx) => { const citations: Citation[] = citedSourceIndices.map((idx) => {
const group = sourceGroups.find((g) => g.docIndex === idx); const group: SourceGroup | undefined = sourceGroups.find((g) => g.docIndex === idx);
return group return group
? { ? {
sourceIndex: idx, sourceIndex: idx,
@@ -77,15 +94,17 @@ router.post('/', async (req: Request<unknown, unknown, QueryBody>, res: Response
name: group.name, name: group.name,
chunkTexts: group.chunks.map((c) => c.text), chunkTexts: group.chunks.map((c) => c.text),
} }
: { sourceIndex: idx, sourceId: null, name: null, chunkTexts: [] }; : { sourceIndex: idx, sourceId: null, name: null, chunkTexts: [] as string[] };
}); });
res.json({ const response: QueryResponse = {
answer, answer,
citations, citations,
groundednessScore, groundednessScore,
followUpQuestions, followUpQuestions,
}); };
res.json(response);
} catch (err) { } catch (err) {
next(err); next(err);
} }

View File

@@ -5,7 +5,9 @@ import multer from 'multer';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import logger from '../logger.js'; import logger from '../logger.js';
import * as notebookStore from '../stores/notebookStore.js'; import * as notebookStore from '../stores/notebookStore.js';
import type { Source } from '../stores/notebookStore.js';
import { parseFile, chunkText, parseUrl, parseYoutubeUrl, isYoutubeUrl } from '../services/sourceService.js'; import { parseFile, chunkText, parseUrl, parseYoutubeUrl, isYoutubeUrl } from '../services/sourceService.js';
import type { TextChunk } from '../services/sourceService.js';
import { embedTexts, storeChunkEmbeddings } from '../services/retrievalService.js'; import { embedTexts, storeChunkEmbeddings } from '../services/retrievalService.js';
import { triggerPreGeneration } from '../services/preGenerationService.js'; import { triggerPreGeneration } from '../services/preGenerationService.js';
import { import {
@@ -50,11 +52,11 @@ router.post(
requireFile, requireFile,
async (req: NotebookRequest, res: Response, next: NextFunction) => { async (req: NotebookRequest, res: Response, next: NextFunction) => {
try { try {
const sourceId = uuidv4(); const sourceId: string = uuidv4();
const rawName = Buffer.from(req.file!.originalname, 'latin1').toString('utf-8'); const rawName: string = Buffer.from(req.file!.originalname, 'latin1').toString('utf-8');
const displayName = cleanFilename(rawName); const displayName: string = cleanFilename(rawName);
const text = await parseFile(req.file!.buffer, req.file!.mimetype, displayName); const text: string = await parseFile(req.file!.buffer, req.file!.mimetype, displayName);
const chunks = chunkText(text, sourceId); const chunks: TextChunk[] = chunkText(text, sourceId);
logger.info( logger.info(
{ {
@@ -67,11 +69,11 @@ router.post(
notebookStore.addChunksToNotebook(req.notebookId!, chunks); notebookStore.addChunksToNotebook(req.notebookId!, chunks);
const texts = chunks.map((c) => c.text); const texts: string[] = chunks.map((c) => c.text);
const embeddings = await embedTexts(texts, 'document'); const embeddings: number[][] = await embedTexts(texts, 'document');
storeChunkEmbeddings(chunks, embeddings); storeChunkEmbeddings(chunks, embeddings);
const source = notebookStore.addSource(req.notebookId!, { const source: Source | null = notebookStore.addSource(req.notebookId!, {
id: sourceId, id: sourceId,
name: displayName, name: displayName,
mimetype: req.file!.mimetype, mimetype: req.file!.mimetype,
@@ -100,14 +102,14 @@ router.post(
async (req: NotebookRequest & Request<unknown, unknown, UrlBody>, res: Response, next: NextFunction) => { async (req: NotebookRequest & Request<unknown, unknown, UrlBody>, res: Response, next: NextFunction) => {
try { try {
const { url } = req.body; const { url } = req.body;
const sourceId = uuidv4(); const sourceId: string = uuidv4();
const isYT = isYoutubeUrl(url!); const isYT: boolean = isYoutubeUrl(url!);
const displayName = isYT ? `YouTube: ${url}` : url!.replace(/^https?:\/\//, '').slice(0, 60); const displayName: string = isYT ? `YouTube: ${url}` : url!.replace(/^https?:\/\//, '').slice(0, 60);
logger.info({ sourceId, url, isYT }, 'processing URL source'); logger.info({ sourceId, url, isYT }, 'processing URL source');
const text = isYT ? await parseYoutubeUrl(url!) : await parseUrl(url!); const text: string = isYT ? await parseYoutubeUrl(url!) : await parseUrl(url!);
const chunks = chunkText(text, sourceId); const chunks: TextChunk[] = chunkText(text, sourceId);
if (chunks.length === 0) { if (chunks.length === 0) {
res.status(422).json({ error: 'No usable text could be extracted from the URL' }); res.status(422).json({ error: 'No usable text could be extracted from the URL' });
@@ -116,11 +118,11 @@ router.post(
notebookStore.addChunksToNotebook(req.notebookId!, chunks); notebookStore.addChunksToNotebook(req.notebookId!, chunks);
const texts = chunks.map((c) => c.text); const texts: string[] = chunks.map((c) => c.text);
const embeddings = await embedTexts(texts, 'document'); const embeddings: number[][] = await embedTexts(texts, 'document');
storeChunkEmbeddings(chunks, embeddings); storeChunkEmbeddings(chunks, embeddings);
const source = notebookStore.addSource(req.notebookId!, { const source: Source | null = notebookStore.addSource(req.notebookId!, {
id: sourceId, id: sourceId,
name: displayName, name: displayName,
mimetype: isYT ? 'video/youtube' : 'text/html', mimetype: isYT ? 'video/youtube' : 'text/html',

View File

@@ -1,10 +1,10 @@
'use strict'; 'use strict';
interface Source { export interface CacheSource {
id: string; id: string;
} }
interface CacheEntry { export interface CacheEntry {
document: unknown; document: unknown;
sourceHash: string; sourceHash: string;
createdAt: number; createdAt: number;
@@ -12,7 +12,7 @@ interface CacheEntry {
const cache = new Map<string, CacheEntry | boolean>(); const cache = new Map<string, CacheEntry | boolean>();
function buildSourceHash(sources: Source[]): string { function buildSourceHash(sources: CacheSource[]): string {
return sources return sources
.map((s) => s.id) .map((s) => s.id)
.sort() .sort()
@@ -29,7 +29,7 @@ export function setCachedDocument(
notebookId: string, notebookId: string,
type: string, type: string,
document: unknown, document: unknown,
sources: Source[] sources: CacheSource[]
): void { ): void {
const key = `${notebookId}:${type}`; const key = `${notebookId}:${type}`;
cache.set(key, { cache.set(key, {
@@ -39,7 +39,7 @@ export function setCachedDocument(
}); });
} }
export function isFresh(notebookId: string, type: string, currentSources: Source[]): boolean { export function isFresh(notebookId: string, type: string, currentSources: CacheSource[]): boolean {
const entry = cache.get(`${notebookId}:${type}`); const entry = cache.get(`${notebookId}:${type}`);
if (!entry || typeof entry === 'boolean') return false; if (!entry || typeof entry === 'boolean') return false;
return entry.sourceHash === buildSourceHash(currentSources); return entry.sourceHash === buildSourceHash(currentSources);