continue refactor of backend to utilize ts, deprecate js
This commit is contained in:
@@ -1,19 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
import { Router } from 'express';
|
||||
import { Router, type Request, type Response, type NextFunction } from 'express';
|
||||
import logger from '../logger.js';
|
||||
import { generateCitationDetail } from '../services/generationService.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/', async (req, res, next) => {
|
||||
interface CitationDetailBody {
|
||||
chunkTexts?: string[];
|
||||
sourceName?: string;
|
||||
answer?: string;
|
||||
citationIndex?: number;
|
||||
}
|
||||
|
||||
router.post('/', async (req: Request<unknown, unknown, CitationDetailBody>, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { chunkTexts, sourceName, answer, citationIndex } = req.body;
|
||||
|
||||
if (!chunkTexts?.length || !answer || citationIndex == null) {
|
||||
return res.status(400).json({
|
||||
res.status(400).json({
|
||||
error: 'chunkTexts, answer, and citationIndex are required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info({ citationIndex, sourceName }, 'citation detail requested');
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
import { Router } from 'express';
|
||||
import { Router, type Request, type Response, type NextFunction } from 'express';
|
||||
import logger from '../logger.js';
|
||||
import * as notebookStore from '../stores/notebookStore.js';
|
||||
import * as documentCacheStore from '../stores/documentCacheStore.js';
|
||||
@@ -8,9 +8,16 @@ import {
|
||||
generateStudyGuide,
|
||||
generateFaq,
|
||||
generateExecutiveBrief,
|
||||
type SourceGroup,
|
||||
type StudyGuide,
|
||||
type Faq,
|
||||
type ExecutiveBrief,
|
||||
} from '../services/documentService.js';
|
||||
|
||||
const GENERATORS = {
|
||||
type DocumentType = 'study-guide' | 'faq' | 'executive-brief';
|
||||
type GeneratorFn = (sourceGroups: SourceGroup[]) => Promise<StudyGuide | Faq | ExecutiveBrief>;
|
||||
|
||||
const GENERATORS: Record<DocumentType, GeneratorFn> = {
|
||||
'study-guide': generateStudyGuide,
|
||||
'faq': generateFaq,
|
||||
'executive-brief': generateExecutiveBrief,
|
||||
@@ -18,36 +25,48 @@ const GENERATORS = {
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/generate', async (req, res, next) => {
|
||||
interface GenerateBody {
|
||||
notebookId?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
router.post('/generate', async (req: Request<unknown, unknown, GenerateBody>, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { notebookId, type } = req.body;
|
||||
|
||||
if (!notebookId || !type) {
|
||||
return res.status(400).json({ error: 'notebookId and type are required' });
|
||||
res.status(400).json({ error: 'notebookId and type are required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const generator = GENERATORS[type];
|
||||
const generator = GENERATORS[type as DocumentType];
|
||||
if (!generator) {
|
||||
return res.status(400).json({
|
||||
res.status(400).json({
|
||||
error: `Invalid type. Must be one of: ${Object.keys(GENERATORS).join(', ')}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks = notebookStore.getChunksForNotebook(notebookId);
|
||||
if (chunks.length === 0) {
|
||||
return 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;
|
||||
}
|
||||
|
||||
const sources = notebookStore.getSources(notebookId);
|
||||
const cached = documentCacheStore.getCachedDocument(notebookId, type);
|
||||
if (cached && documentCacheStore.isFresh(notebookId, type, sources)) {
|
||||
logger.info({ notebookId, type }, 'serving cached document');
|
||||
return res.json({ type, document: cached.document });
|
||||
res.json({ type, document: cached.document });
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceGroups = notebookStore.buildSourceGroups(notebookId, chunks);
|
||||
|
||||
logger.info({ notebookId, type, sourceCount: sourceGroups.length, chunkCount: chunks.length }, 'document generation started');
|
||||
logger.info(
|
||||
{ notebookId, type, sourceCount: sourceGroups.length, chunkCount: chunks.length },
|
||||
'document generation started'
|
||||
);
|
||||
|
||||
const document = await generator(sourceGroups);
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
import { Router } from 'express';
|
||||
import { Router, type Request, type Response } from 'express';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import * as notebookStore from '../stores/notebookStore.js';
|
||||
import { deleteVectorsForChunks } from '../stores/vectorStore.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
router.get('/', (_req: Request, res: Response) => {
|
||||
res.json(notebookStore.getAllNotebooks());
|
||||
});
|
||||
|
||||
router.post('/', (req, res) => {
|
||||
interface CreateNotebookBody {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
router.post('/', (req: Request<unknown, unknown, CreateNotebookBody>, res: Response) => {
|
||||
const { name } = req.body;
|
||||
if (!name || !name.trim()) {
|
||||
return res.status(400).json({ error: 'name is required' });
|
||||
res.status(400).json({ error: 'name is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const notebook = notebookStore.createNotebook({
|
||||
@@ -26,11 +31,16 @@ router.post('/', (req, res) => {
|
||||
res.status(201).json(notebook);
|
||||
});
|
||||
|
||||
router.delete('/:id', (req, res) => {
|
||||
interface DeleteParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
router.delete('/:id', (req: Request<DeleteParams>, res: Response) => {
|
||||
const chunks = notebookStore.getChunksForNotebook(req.params.id);
|
||||
const deleted = notebookStore.deleteNotebook(req.params.id);
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ error: 'notebook not found' });
|
||||
res.status(404).json({ error: 'notebook not found' });
|
||||
return;
|
||||
}
|
||||
if (chunks.length) {
|
||||
deleteVectorsForChunks(chunks.map((c) => c.id));
|
||||
@@ -39,4 +49,3 @@ router.delete('/:id', (req, res) => {
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
import { Router } from 'express';
|
||||
import { Router, type Request, type Response, type NextFunction } from 'express';
|
||||
import logger from '../logger.js';
|
||||
import { embedTexts, search, rerank } from '../services/retrievalService.js';
|
||||
import { generate } from '../services/generationService.js';
|
||||
@@ -12,11 +12,17 @@ const TOP_K_RERANK = 5;
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/', async (req, res, next) => {
|
||||
interface QueryBody {
|
||||
notebookId?: string;
|
||||
question?: string;
|
||||
}
|
||||
|
||||
router.post('/', async (req: Request<unknown, unknown, QueryBody>, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { notebookId, question } = req.body;
|
||||
if (!notebookId || !question) {
|
||||
return res.status(400).json({ error: 'notebookId and question are required' });
|
||||
res.status(400).json({ error: 'notebookId and question are required' });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info({ notebookId, question }, 'query received');
|
||||
@@ -25,31 +31,32 @@ router.post('/', async (req, res, next) => {
|
||||
|
||||
const searchResults = search(queryEmbedding, notebookId, TOP_K_SEARCH);
|
||||
if (searchResults.length === 0) {
|
||||
return res.json({
|
||||
res.json({
|
||||
answer: 'No sources found for this notebook. Upload some documents first.',
|
||||
citations: [],
|
||||
groundednessScore: null,
|
||||
followUpQuestions: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const candidateChunks = searchResults.map((r) => r.chunk);
|
||||
const reranked = await rerank(question, candidateChunks);
|
||||
const topChunks = reranked.slice(0, TOP_K_RERANK).map((r) => r.chunk);
|
||||
|
||||
logger.debug({
|
||||
searchHits: searchResults.length,
|
||||
rerankedTop: topChunks.length,
|
||||
}, 'retrieval complete');
|
||||
logger.debug(
|
||||
{
|
||||
searchHits: searchResults.length,
|
||||
rerankedTop: topChunks.length,
|
||||
},
|
||||
'retrieval complete'
|
||||
);
|
||||
|
||||
const sourceGroups = notebookStore.buildSourceGroups(notebookId, topChunks);
|
||||
|
||||
const { answer, citedSourceIndices, followUpQuestions } = await generate(
|
||||
question,
|
||||
sourceGroups,
|
||||
);
|
||||
const { answer, citedSourceIndices, followUpQuestions } = await generate(question, sourceGroups);
|
||||
|
||||
const citedChunkIds = [];
|
||||
const citedChunkIds: string[] = [];
|
||||
for (const idx of citedSourceIndices) {
|
||||
const group = sourceGroups.find((g) => g.docIndex === idx);
|
||||
if (group) {
|
||||
@@ -65,11 +72,11 @@ router.post('/', async (req, res, next) => {
|
||||
const group = sourceGroups.find((g) => g.docIndex === idx);
|
||||
return group
|
||||
? {
|
||||
sourceIndex: idx,
|
||||
sourceId: group.sourceId,
|
||||
name: group.name,
|
||||
chunkTexts: group.chunks.map((c) => c.text),
|
||||
}
|
||||
sourceIndex: idx,
|
||||
sourceId: group.sourceId,
|
||||
name: group.name,
|
||||
chunkTexts: group.chunks.map((c) => c.text),
|
||||
}
|
||||
: { sourceIndex: idx, sourceId: null, name: null, chunkTexts: [] };
|
||||
});
|
||||
|
||||
@@ -85,4 +92,3 @@ router.post('/', async (req, res, next) => {
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
import { Router } from 'express';
|
||||
import { Router, type Request, type Response, type NextFunction } from 'express';
|
||||
import multer from 'multer';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import logger from '../logger.js';
|
||||
import * as notebookStore 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 { embedTexts, storeChunkEmbeddings } from '../services/retrievalService.js';
|
||||
import { triggerPreGeneration } from '../services/preGenerationService.js';
|
||||
import {
|
||||
@@ -19,28 +13,30 @@ import {
|
||||
requireNotebook,
|
||||
requireFile,
|
||||
requireUrl,
|
||||
type NotebookRequest,
|
||||
} from '../middleware/validation.js';
|
||||
|
||||
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });
|
||||
const router = Router();
|
||||
|
||||
const TIMESTAMP_RE = /\s+\d{1,2}\.\d{2}\.\d{2}\s*[\u2018\u2019''\s]?\s*[AP]M(?=\.\w+$)/i;
|
||||
function cleanFilename(name) {
|
||||
function cleanFilename(name: string): string {
|
||||
return name.replace(TIMESTAMP_RE, '');
|
||||
}
|
||||
|
||||
router.get('/', requireNotebookId, (req, res) => {
|
||||
res.json(notebookStore.getSources(req.notebookId));
|
||||
router.get('/', requireNotebookId, (req: NotebookRequest, res: Response) => {
|
||||
res.json(notebookStore.getSources(req.notebookId!));
|
||||
});
|
||||
|
||||
|
||||
function multerUpload(req, res, next) {
|
||||
function multerUpload(req: Request, res: Response, next: NextFunction): void {
|
||||
upload.single('file')(req, res, (err) => {
|
||||
if (err) {
|
||||
if (err.code === 'LIMIT_FILE_SIZE') {
|
||||
return res.status(413).json({ error: 'File too large. Maximum size is 50 MB.' });
|
||||
if ((err as multer.MulterError).code === 'LIMIT_FILE_SIZE') {
|
||||
res.status(413).json({ error: 'File too large. Maximum size is 50 MB.' });
|
||||
return;
|
||||
}
|
||||
return next(err);
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
@@ -52,73 +48,79 @@ router.post(
|
||||
requireNotebookId,
|
||||
requireNotebook,
|
||||
requireFile,
|
||||
async (req, res, next) => {
|
||||
async (req: NotebookRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const sourceId = uuidv4();
|
||||
const rawName = Buffer.from(req.file.originalname, 'latin1').toString('utf-8');
|
||||
const rawName = Buffer.from(req.file!.originalname, 'latin1').toString('utf-8');
|
||||
const displayName = cleanFilename(rawName);
|
||||
const text = await parseFile(req.file.buffer, req.file.mimetype, displayName);
|
||||
const text = await parseFile(req.file!.buffer, req.file!.mimetype, displayName);
|
||||
const chunks = chunkText(text, sourceId);
|
||||
|
||||
logger.info({
|
||||
sourceId,
|
||||
filename: displayName,
|
||||
chunkCount: chunks.length,
|
||||
}, 'parsed and chunked source');
|
||||
logger.info(
|
||||
{
|
||||
sourceId,
|
||||
filename: displayName,
|
||||
chunkCount: chunks.length,
|
||||
},
|
||||
'parsed and chunked source'
|
||||
);
|
||||
|
||||
notebookStore.addChunksToNotebook(req.notebookId, chunks);
|
||||
notebookStore.addChunksToNotebook(req.notebookId!, chunks);
|
||||
|
||||
const texts = chunks.map((c) => c.text);
|
||||
const embeddings = await embedTexts(texts, 'document');
|
||||
storeChunkEmbeddings(chunks, embeddings);
|
||||
|
||||
const source = notebookStore.addSource(req.notebookId, {
|
||||
const source = notebookStore.addSource(req.notebookId!, {
|
||||
id: sourceId,
|
||||
name: displayName,
|
||||
mimetype: req.file.mimetype,
|
||||
mimetype: req.file!.mimetype,
|
||||
chunkCount: chunks.length,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
triggerPreGeneration(req.notebookId);
|
||||
triggerPreGeneration(req.notebookId!);
|
||||
|
||||
res.status(201).json(source);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
interface UrlBody {
|
||||
url?: string;
|
||||
}
|
||||
|
||||
router.post(
|
||||
'/url',
|
||||
requireNotebookId,
|
||||
requireNotebook,
|
||||
requireUrl,
|
||||
async (req, res, next) => {
|
||||
async (req: NotebookRequest & Request<unknown, unknown, UrlBody>, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { url } = req.body;
|
||||
const sourceId = uuidv4();
|
||||
const isYT = isYoutubeUrl(url);
|
||||
const displayName = isYT
|
||||
? `YouTube: ${url}`
|
||||
: url.replace(/^https?:\/\//, '').slice(0, 60);
|
||||
const isYT = isYoutubeUrl(url!);
|
||||
const displayName = isYT ? `YouTube: ${url}` : url!.replace(/^https?:\/\//, '').slice(0, 60);
|
||||
|
||||
logger.info({ sourceId, url, isYT }, 'processing URL source');
|
||||
|
||||
const text = isYT ? await parseYoutubeUrl(url) : await parseUrl(url);
|
||||
const text = isYT ? await parseYoutubeUrl(url!) : await parseUrl(url!);
|
||||
const chunks = chunkText(text, sourceId);
|
||||
|
||||
if (chunks.length === 0) {
|
||||
return 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' });
|
||||
return;
|
||||
}
|
||||
|
||||
notebookStore.addChunksToNotebook(req.notebookId, chunks);
|
||||
notebookStore.addChunksToNotebook(req.notebookId!, chunks);
|
||||
|
||||
const texts = chunks.map((c) => c.text);
|
||||
const embeddings = await embedTexts(texts, 'document');
|
||||
storeChunkEmbeddings(chunks, embeddings);
|
||||
|
||||
const source = notebookStore.addSource(req.notebookId, {
|
||||
const source = notebookStore.addSource(req.notebookId!, {
|
||||
id: sourceId,
|
||||
name: displayName,
|
||||
mimetype: isYT ? 'video/youtube' : 'text/html',
|
||||
@@ -126,13 +128,13 @@ router.post(
|
||||
uploadedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
triggerPreGeneration(req.notebookId);
|
||||
triggerPreGeneration(req.notebookId!);
|
||||
|
||||
res.status(201).json(source);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user