diff --git a/services/platform-service/src/modules/knowledge/repository.ts b/services/platform-service/src/modules/knowledge/repository.ts index d633a3f6..0fc28122 100644 --- a/services/platform-service/src/modules/knowledge/repository.ts +++ b/services/platform-service/src/modules/knowledge/repository.ts @@ -105,3 +105,65 @@ export async function listChunks( limit: filter.limit ?? 500, }); } + +export async function searchChunks( + knowledgeBaseId: string, + query: string, + options: { sourceId?: string; limit?: number } = {} +): Promise { + const allChunks = await listChunks(knowledgeBaseId, { + sourceId: options.sourceId, + limit: 1000, + }); + + const queryLower = query.toLowerCase(); + const terms = queryLower.split(/\s+/).filter(Boolean); + + const scored = allChunks + .map(chunk => { + const text = chunk.contentText.toLowerCase(); + let score = 0; + for (const term of terms) { + if (text.includes(term)) score++; + } + return { chunk, score }; + }) + .filter(s => s.score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, options.limit ?? 10); + + return scored.map(s => s.chunk); +} + +export interface KnowledgeBaseStats { + knowledgeBaseId: string; + sourceCount: number; + chunkCount: number; + totalTokens: number; + indexedSources: number; + pendingSources: number; + failedSources: number; +} + +export async function getBaseStats(knowledgeBaseId: string): Promise { + const sources = await listSources(knowledgeBaseId); + const chunks = await listChunks(knowledgeBaseId, { limit: 10000 }); + + return { + knowledgeBaseId, + sourceCount: sources.length, + chunkCount: chunks.length, + totalTokens: chunks.reduce((sum, c) => sum + (c.tokenCount ?? 0), 0), + indexedSources: sources.filter(s => s.status === 'indexed').length, + pendingSources: sources.filter(s => s.status === 'pending').length, + failedSources: sources.filter(s => s.status === 'failed').length, + }; +} + +export async function deleteSource(id: string, knowledgeBaseId: string): Promise { + await sourceCollection().delete(id, knowledgeBaseId); +} + +export async function deleteBase(id: string, productId: string): Promise { + await baseCollection().delete(id, productId); +} diff --git a/services/platform-service/src/modules/knowledge/routes.ts b/services/platform-service/src/modules/knowledge/routes.ts index 35148563..cfadeedd 100644 --- a/services/platform-service/src/modules/knowledge/routes.ts +++ b/services/platform-service/src/modules/knowledge/routes.ts @@ -209,4 +209,61 @@ export async function knowledgeRoutes(app: FastifyInstance) { })), }; }); + + // ── Knowledge base stats ─────────────────────────────── + app.get('/knowledge/bases/:id/stats', async req => { + const access = requireAdmin(req); + const { id } = req.params as { id: string }; + await repo.getBase(id, access.productId); + return repo.getBaseStats(id); + }); + + // ── Delete knowledge base (draft only) ───────────────── + app.delete('/knowledge/bases/:id', async req => { + const access = requireAdmin(req); + const { id } = req.params as { id: string }; + const base = await repo.getBase(id, access.productId); + if (base.status !== 'draft') { + throw new BadRequestError('Only draft knowledge bases can be deleted'); + } + await repo.deleteBase(id, access.productId); + return { deleted: true }; + }); + + // ── Delete knowledge source ──────────────────────────── + app.delete('/knowledge/bases/:id/sources/:sourceId', async req => { + const access = requireAdmin(req); + const { id, sourceId } = req.params as { id: string; sourceId: string }; + await repo.getBase(id, access.productId); + await repo.getSource(sourceId, id); + await repo.deleteSource(sourceId, id); + return { deleted: true }; + }); + + // ── Search chunks (text search) ──────────────────────── + app.post('/knowledge/bases/:id/search', async req => { + requireAdmin(req); + const { id } = req.params as { id: string }; + const parsed = RetrievalQuerySchema.safeParse(req.body); + if (!parsed.success) { + validationError(parsed.error.issues.map(issue => issue.message).join('; ')); + } + const results = await repo.searchChunks(id, parsed.data.query, { + sourceId: parsed.data.sourceId, + limit: parsed.data.limit, + }); + return { + query: parsed.data.query, + count: results.length, + chunks: results.map(c => ({ + id: c.id, + sourceId: c.sourceId, + ordinal: c.ordinal, + preview: c.contentText.slice(0, 240), + tokenCount: c.tokenCount, + citations: c.citations, + tags: c.tags, + })), + }; + }); }