/** * Project Context Builder for Chat * * Loads project state from Firestore and AlloyDB vector memory, * building a compact context object for LLM consumption. */ import { getAdminDb } from '@/lib/firebase/admin'; import { retrieveRelevantChunks } from '@/lib/server/vector-memory'; import { embedText } from '@/lib/ai/embeddings'; import { summarizeKnowledgeItems, summarizeExtractions, } from '@/lib/server/chat-mode-resolver'; import type { ChatMode } from '@/lib/ai/chat-modes'; import type { ProjectPhaseData, ProjectPhaseScores } from '@/lib/types/project-artifacts'; import type { PhaseHandoff } from '@/lib/types/phase-handoff'; /** * Compact project context for LLM */ export interface ProjectChatContext { /** Basic project info */ project: { id: string; name: string; currentPhase: string; phaseStatus: string; githubRepo?: string | null; githubRepoUrl?: string | null; extensionLinked?: boolean; visionAnswers?: { q1?: string; q2?: string; q3?: string; updatedAt?: string; }; }; /** Phase-specific artifacts */ phaseData: { canonicalProductModel?: any; mvpPlan?: any; marketingPlan?: any; }; /** Phase scores and progress */ phaseScores: ProjectPhaseScores; /** Phase handoffs for smart transitions */ phaseHandoffs: Partial>; /** Knowledge summary (counts, types) */ knowledgeSummary: { totalCount: number; bySourceType: Record; recentTitles: string[]; }; /** Extraction summary */ extractionSummary: { totalCount: number; avgConfidence: number; avgCompletion: number; }; /** Relevant chunks from vector search */ retrievedChunks: { content: string; sourceType?: string | null; importance?: string | null; similarity: number; }[]; /** Repository analysis (if GitHub connected) */ repositoryAnalysis?: { repoFullName: string; totalFiles: number; directories: string[]; keyFiles: string[]; techStack: string[]; readme: string | null; summary: string; } | null; /** Session history from linked Cursor sessions */ sessionHistory: { totalSessions: number; messages: Array<{ role: string; content: string; timestamp: string; sessionId?: string; }>; }; } /** * Build project context for a chat interaction * * @param projectId - Firestore project ID * @param mode - Current chat mode * @param userMessage - User's message (used for vector retrieval) * @param options - Context building options * @returns Compact context object */ export async function buildProjectContextForChat( projectId: string, mode: ChatMode, userMessage: string, options: { retrievalLimit?: number; includeVectorSearch?: boolean; includeGitHubAnalysis?: boolean; } = {} ): Promise { const { retrievalLimit = 10, includeVectorSearch = true, includeGitHubAnalysis = true, } = options; try { const adminDb = getAdminDb(); // Load project document const projectSnapshot = await adminDb.collection('projects').doc(projectId).get(); if (!projectSnapshot.exists) { throw new Error(`Project ${projectId} not found`); } const projectData = projectSnapshot.data() ?? {}; // Load summaries in parallel const [knowledgeSummary, extractionSummary] = await Promise.all([ summarizeKnowledgeItems(projectId), summarizeExtractions(projectId), ]); // Vector retrieval let retrievedChunks: ProjectChatContext['retrievedChunks'] = []; // extraction_review_mode does NOT load documents - it reviews extraction results // Normal vector search for modes that need it if (includeVectorSearch && mode !== 'collector_mode' && mode !== 'extraction_review_mode' && userMessage.trim().length > 0) { try { const queryEmbedding = await embedText(userMessage); const chunks = await retrieveRelevantChunks(projectId, queryEmbedding, { limit: retrievalLimit, minSimilarity: 0.7, // Only include reasonably relevant chunks }); retrievedChunks = chunks.map((chunk) => ({ content: chunk.content, sourceType: chunk.sourceType, importance: chunk.importance, similarity: chunk.similarity, })); console.log( `[Chat Context] Retrieved ${retrievedChunks.length} chunks for project ${projectId}` ); } catch (vectorError) { console.error('[Chat Context] Vector retrieval failed:', vectorError); // Continue without vector results } } // GitHub repository analysis let repositoryAnalysis = null; if (includeGitHubAnalysis && projectData.githubRepo && projectData.userId) { try { const { analyzeGitHubRepository } = await import('@/lib/server/github-analyzer'); repositoryAnalysis = await analyzeGitHubRepository( projectData.userId, projectData.githubRepo, projectData.githubDefaultBranch || 'main' ); } catch (githubError) { console.error('[Chat Context] GitHub analysis failed:', githubError); // Continue without GitHub analysis } } // Fetch linked Cursor session history let sessionHistory = { totalSessions: 0, messages: [] as Array<{ role: string; content: string; timestamp: string; sessionId?: string; }>, }; try { // Query sessions linked to this project const sessionsSnapshot = await adminDb .collection('sessions') .where('projectId', '==', projectId) .orderBy('startTime', 'asc') .get(); if (!sessionsSnapshot.empty) { sessionHistory.totalSessions = sessionsSnapshot.size; // Extract all messages from all sessions in chronological order const allMessages: Array<{ role: string; content: string; timestamp: string; sessionId: string; }> = []; for (const sessionDoc of sessionsSnapshot.docs) { const sessionData = sessionDoc.data(); const conversation = sessionData.conversation || []; // Add messages from this session for (const msg of conversation) { if (msg.content && msg.content.trim()) { allMessages.push({ role: msg.role || 'unknown', content: msg.content, timestamp: msg.timestamp instanceof Date ? msg.timestamp.toISOString() : (typeof msg.timestamp === 'string' ? msg.timestamp : new Date().toISOString()), sessionId: sessionDoc.id, }); } } } // Sort all messages by timestamp (chronological order) allMessages.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() ); sessionHistory.messages = allMessages; console.log( `[Chat Context] Loaded ${sessionHistory.totalSessions} sessions with ${allMessages.length} total messages for project ${projectId}` ); } else { console.log(`[Chat Context] No linked sessions found for project ${projectId}`); } } catch (sessionError) { console.error('[Chat Context] Session history fetch failed:', sessionError); // Continue without session history } // Build context object const context: ProjectChatContext = { project: { id: projectId, name: projectData.name ?? 'Unnamed Project', currentPhase: projectData.currentPhase ?? 'collector', phaseStatus: projectData.phaseStatus ?? 'not_started', githubRepo: projectData.githubRepo ?? null, githubRepoUrl: projectData.githubRepoUrl ?? null, extensionLinked: projectData.extensionLinked ?? false, visionAnswers: projectData.visionAnswers ?? {}, }, phaseData: { canonicalProductModel: projectData.phaseData?.canonicalProductModel ?? null, mvpPlan: projectData.phaseData?.mvpPlan ?? null, marketingPlan: projectData.phaseData?.marketingPlan ?? null, }, phaseScores: projectData.phaseScores ?? {}, phaseHandoffs: projectData.phaseData?.phaseHandoffs ?? {}, knowledgeSummary, extractionSummary, retrievedChunks, repositoryAnalysis: repositoryAnalysis as any, sessionHistory, // ✅ Include session history in context }; return context; } catch (error) { console.error('[Chat Context] Failed to build context:', error); throw new Error( `Failed to build chat context: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Determine which artifacts were used in building the context * * This helps the UI show what sources the AI is drawing from. */ export function determineArtifactsUsed(context: ProjectChatContext): string[] { const artifacts: string[] = []; if (context.phaseData.canonicalProductModel) { artifacts.push('Product Model'); } if (context.phaseData.mvpPlan) { artifacts.push('MVP Plan'); } if (context.phaseData.marketingPlan) { artifacts.push('Marketing Plan'); } if (context.retrievedChunks.length > 0) { artifacts.push(`${context.retrievedChunks.length} Vector Chunks`); } if (context.repositoryAnalysis) { artifacts.push('GitHub Repo Analysis'); } if (context.knowledgeSummary.totalCount > 0) { artifacts.push(`${context.knowledgeSummary.totalCount} Knowledge Items`); } if (context.extractionSummary.totalCount > 0) { artifacts.push(`${context.extractionSummary.totalCount} Extractions`); } if (context.sessionHistory.totalSessions > 0) { artifacts.push(`${context.sessionHistory.totalSessions} Cursor Sessions (${context.sessionHistory.messages.length} messages)`); } return artifacts; } /** * Format project context as a string for LLM system prompt * * Provides a human-readable summary of the context. */ export function formatContextForPrompt(context: ProjectChatContext): string { const sections: string[] = []; // Project info sections.push(`Project: ${context.project.name} (ID: ${context.project.id})`); sections.push( `Phase: ${context.project.currentPhase} (${context.project.phaseStatus})` ); // Knowledge summary if (context.knowledgeSummary.totalCount > 0) { sections.push(`\nKnowledge Items: ${context.knowledgeSummary.totalCount} total`); if (Object.keys(context.knowledgeSummary.bySourceType).length > 0) { sections.push( ` By type: ${JSON.stringify(context.knowledgeSummary.bySourceType)}` ); } } // Extraction summary if (context.extractionSummary.totalCount > 0) { sections.push( `\nExtractions: ${context.extractionSummary.totalCount} analyzed (avg confidence: ${(context.extractionSummary.avgConfidence * 100).toFixed(1)}%)` ); } // Retrieved chunks if (context.retrievedChunks.length > 0) { sections.push(`\nRelevant Context (vector search):`); context.retrievedChunks.slice(0, 3).forEach((chunk, i) => { sections.push( ` ${i + 1}. [${chunk.sourceType ?? 'unknown'}] (similarity: ${(chunk.similarity * 100).toFixed(1)}%)` ); sections.push(` ${chunk.content.substring(0, 150)}...`); }); } // GitHub repo if (context.repositoryAnalysis) { sections.push(`\nGitHub Repository: ${context.repositoryAnalysis.repoFullName}`); sections.push(` Files: ${context.repositoryAnalysis.totalFiles}`); sections.push(` Tech: ${context.repositoryAnalysis.techStack.join(', ')}`); } // Phase handoffs const handoffs = Object.keys(context.phaseHandoffs); if (handoffs.length > 0) { sections.push(`\nPhase Handoffs: ${handoffs.join(', ')}`); } // Session history if (context.sessionHistory.totalSessions > 0) { sections.push(`\n## Cursor Session History (${context.sessionHistory.totalSessions} sessions, ${context.sessionHistory.messages.length} messages)`); sections.push(`This is your complete conversation history with the user from Cursor IDE, in chronological order.`); sections.push(`Use this to understand what has been built, discussed, and decided so far.\n`); // Include all messages chronologically context.sessionHistory.messages.forEach((msg, i) => { const timestamp = new Date(msg.timestamp).toLocaleString(); sections.push(`[${timestamp}] ${msg.role}:`); sections.push(msg.content); sections.push(''); // Empty line between messages }); } return sections.join('\n'); }