// convex/knowledgeBase.ts import { query, mutation, action, internalQuery, internalMutation, internalAction, } from "./_generated/server"; import { v } from "convex/values"; import { getAuthUserId } from "@convex-dev/auth/server"; import { internal, components } from "./_generated/api"; import { RAG } from "@convex-dev/rag"; import { openai } from "@ai-sdk/openai"; // Initialize RAG (OpenAI embeddings) const rag = new RAG(components.rag, { textEmbeddingModel: openai.embedding("text-embedding-3-small"), embeddingDimension: 1536, }); export const generateUploadUrl = mutation({ args: {}, handler: async (ctx) => { const userId = await getAuthUserId(ctx); if (!userId) throw new Error("Not authenticated"); return await ctx.storage.generateUploadUrl(); }, }); export const uploadDocument = mutation({ args: { filename: v.string(), storageId: v.id("_storage"), }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); if (!userId) throw new Error("Not authenticated"); const documentId = await ctx.db.insert("documents", { filename: args.filename, storageId: args.storageId, uploadedBy: userId, uploadedAt: Date.now(), status: "processing", }); // Schedule background processing await ctx.scheduler.runAfter(0, internal.knowledgeBase.processDocument, { documentId, }); return documentId; }, }); export const processDocument = internalAction({ args: { documentId: v.id("documents") }, handler: async (ctx, { documentId }) => { const document = await ctx.runQuery(internal.knowledgeBase.getDocument, { documentId, }); if (!document) throw new Error("Document not found"); try { // Get uploaded file const file = await ctx.storage.get(document.storageId); if (!file) throw new Error("File not found in storage"); const buffer = await file.arrayBuffer(); // 🔹 For now: fake extracted text (replace with pdf-parse or OCR in real impl) const text = `Extracted content from ${document.filename}. (Simulated text extraction for now.)`; // Add text to RAG namespace (per-user isolation) await rag.add(ctx, { namespace: document.uploadedBy, text, metadata: { filename: document.filename }, }); // Update status await ctx.runMutation(internal.knowledgeBase.updateDocumentStatus, { documentId, status: "completed", }); } catch (error) { console.error("Error processing document:", error); await ctx.runMutation(internal.knowledgeBase.updateDocumentStatus, { documentId, status: "failed", }); } }, }); export const getDocument = internalQuery({ args: { documentId: v.id("documents") }, handler: async (ctx, { documentId }) => { return await ctx.db.get(documentId); }, }); export const updateDocumentStatus = internalMutation({ args: { documentId: v.id("documents"), status: v.string(), }, handler: async (ctx, { documentId, status }) => { await ctx.db.patch(documentId, { status }); }, }); export const listDocuments = query({ args: {}, handler: async (ctx) => { const userId = await getAuthUserId(ctx); if (!userId) return []; return await ctx.db .query("documents") .withIndex("by_uploaded_by", (q) => q.eq("uploadedBy", userId)) .order("desc") .collect(); }, }); export const searchKnowledge = action({ args: { query: v.string(), limit: v.optional(v.number()), }, handler: async (ctx, { query, limit }) => { const userId = await getAuthUserId(ctx); if (!userId) throw new Error("Not authenticated"); return await rag.search(ctx, { namespace: userId, // keep user’s knowledge separate query, limit: limit ?? 3, }); }, });