VIBN Frontend for Coolify deployment
This commit is contained in:
402
lib/server/chat-context.ts
Normal file
402
lib/server/chat-context.ts
Normal file
@@ -0,0 +1,402 @@
|
||||
/**
|
||||
* 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<Record<'collector' | 'extraction' | 'vision' | 'mvp' | 'marketing', PhaseHandoff>>;
|
||||
|
||||
/** Knowledge summary (counts, types) */
|
||||
knowledgeSummary: {
|
||||
totalCount: number;
|
||||
bySourceType: Record<string, number>;
|
||||
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<ProjectChatContext> {
|
||||
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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user