Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
52 lines
2.0 KiB
TypeScript
52 lines
2.0 KiB
TypeScript
import type { FastifyPluginAsync, FastifyRequest } from 'fastify';
|
|
import { z } from 'zod';
|
|
import { prisma } from '../db.js';
|
|
import { checkRateLimit, rateLimitResponse } from '../lib/rate-limit.js';
|
|
|
|
function stripHtmlToMarkdown(html: string): string {
|
|
if (!html) return '';
|
|
return html
|
|
.replace(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, '\n## $1\n')
|
|
.replace(/<p[^>]*>(.*?)<\/p>/gi, '\n$1\n')
|
|
.replace(/<a[^>]*href="([^"]+)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)')
|
|
.replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**')
|
|
.replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*')
|
|
.replace(/<[^>]+>/g, '')
|
|
.replace(/\n\s*\n/g, '\n\n')
|
|
.trim();
|
|
}
|
|
|
|
export const ragRoutes: FastifyPluginAsync = async (app) => {
|
|
app.get('/api/v1/rag/:slug', async (req, reply) => {
|
|
const { slug } = req.params as { slug: string };
|
|
const article = await prisma.article.findUnique({
|
|
where: { slug },
|
|
select: { title: true, summary: true, content: true, dateModified: true }
|
|
});
|
|
|
|
if (!article) return reply.code(404).send('# Not Found');
|
|
|
|
reply.header('Content-Type', 'text/markdown; charset=utf-8');
|
|
|
|
let markdown = `# ${article.title}\n\n`;
|
|
if (article.summary) markdown += `> ${article.summary}\n\n`;
|
|
markdown += `Last Modified: ${article.dateModified?.toISOString()}\n\n`;
|
|
markdown += stripHtmlToMarkdown(article.content);
|
|
|
|
return markdown;
|
|
});
|
|
|
|
app.post('/api/v1/search/semantic', async (req: FastifyRequest, reply) => {
|
|
const rl = await checkRateLimit(req.ip, 'semantic', 10, 60);
|
|
if (rl.limited) return rateLimitResponse(reply, rl.retryAfter);
|
|
|
|
const schema = z.object({ query: z.string().min(1), limit: z.number().max(20).optional() });
|
|
const parsed = schema.safeParse(req.body);
|
|
if (!parsed.success) return reply.code(400).send({ error: 'invalid_input' });
|
|
|
|
const { semanticSearch } = await import('../lib/embeddings.js');
|
|
const results = await semanticSearch(parsed.data.query, parsed.data.limit || 5);
|
|
return { results };
|
|
});
|
|
};
|