import { runQuery } from "~/lib/neo4j.server"; export interface SessionEpisodeData { uuid: string; content: string; originalContent: string; source: string; createdAt: Date; validAt: Date; metadata: any; sessionId: string; } export interface CompactedSessionNode { uuid: string; sessionId: string; summary: string; summaryEmbedding: number[]; episodeCount: number; startTime: Date; endTime: Date; createdAt: Date; updatedAt?: Date; confidence: number; userId: string; source: string; compressionRatio: number; metadata: Record; } /** * Save or update a compacted session */ export async function saveCompactedSession( compact: CompactedSessionNode ): Promise { const query = ` MERGE (cs:CompactedSession {uuid: $uuid}) ON CREATE SET cs.sessionId = $sessionId, cs.summary = $summary, cs.summaryEmbedding = $summaryEmbedding, cs.episodeCount = $episodeCount, cs.startTime = $startTime, cs.endTime = $endTime, cs.createdAt = $createdAt, cs.confidence = $confidence, cs.userId = $userId, cs.source = $source, cs.compressionRatio = $compressionRatio, cs.metadata = $metadata ON MATCH SET cs.summary = $summary, cs.summaryEmbedding = $summaryEmbedding, cs.episodeCount = $episodeCount, cs.endTime = $endTime, cs.updatedAt = $updatedAt, cs.confidence = $confidence, cs.compressionRatio = $compressionRatio, cs.metadata = $metadata RETURN cs.uuid as uuid `; const params = { uuid: compact.uuid, sessionId: compact.sessionId, summary: compact.summary, summaryEmbedding: compact.summaryEmbedding, episodeCount: compact.episodeCount, startTime: compact.startTime.toISOString(), endTime: compact.endTime.toISOString(), createdAt: compact.createdAt.toISOString(), updatedAt: compact.updatedAt?.toISOString() || null, confidence: compact.confidence, userId: compact.userId, source: compact.source, compressionRatio: compact.compressionRatio, metadata: JSON.stringify(compact.metadata || {}), }; const result = await runQuery(query, params); return result[0].get("uuid"); } /** * Get a compacted session by UUID */ export async function getCompactedSession( uuid: string ): Promise { const query = ` MATCH (cs:CompactedSession {uuid: $uuid}) RETURN cs `; const result = await runQuery(query, { uuid }); if (result.length === 0) return null; const compact = result[0].get("cs").properties; return parseCompactedSessionNode(compact); } /** * Get compacted session by sessionId */ export async function getCompactedSessionBySessionId( sessionId: string, userId: string ): Promise { const query = ` MATCH (cs:CompactedSession {sessionId: $sessionId, userId: $userId}) RETURN cs ORDER BY cs.endTime DESC LIMIT 1 `; const result = await runQuery(query, { sessionId, userId }); if (result.length === 0) return null; const compact = result[0].get("cs").properties; return parseCompactedSessionNode(compact); } /** * Get all episodes linked to a compacted session */ export async function getCompactedSessionEpisodes( compactUuid: string ): Promise { const query = ` MATCH (cs:CompactedSession {uuid: $compactUuid})-[:COMPACTS]->(e:Episode) RETURN e.uuid as episodeUuid ORDER BY e.createdAt ASC `; const result = await runQuery(query, { compactUuid }); return result.map((r) => r.get("episodeUuid")); } /** * Link episodes to compacted session */ export async function linkEpisodesToCompact( compactUuid: string, episodeUuids: string[], userId: string ): Promise { const query = ` MATCH (cs:CompactedSession {uuid: $compactUuid, userId: $userId}) UNWIND $episodeUuids as episodeUuid MATCH (e:Episode {uuid: episodeUuid, userId: $userId}) MERGE (cs)-[:COMPACTS {createdAt: datetime()}]->(e) MERGE (e)-[:COMPACTED_INTO {createdAt: datetime()}]->(cs) `; await runQuery(query, { compactUuid, episodeUuids, userId }); } /** * Search compacted sessions by embedding similarity */ export async function searchCompactedSessionsByEmbedding( embedding: number[], userId: string, limit: number = 10, minScore: number = 0.7 ): Promise> { const query = ` MATCH (cs:CompactedSession {userId: $userId}) WHERE cs.summaryEmbedding IS NOT NULL WITH cs, gds.similarity.cosine(cs.summaryEmbedding, $embedding) AS score WHERE score >= $minScore RETURN cs, score ORDER BY score DESC LIMIT $limit `; const result = await runQuery(query, { embedding, userId, limit, minScore, }); return result.map((r) => ({ compact: parseCompactedSessionNode(r.get("cs").properties), score: r.get("score"), })); } /** * Get compacted sessions for a user */ export async function getUserCompactedSessions( userId: string, limit: number = 50 ): Promise { const query = ` MATCH (cs:CompactedSession {userId: $userId}) RETURN cs ORDER BY cs.endTime DESC LIMIT $limit `; const result = await runQuery(query, { userId, limit }); return result.map((r) => parseCompactedSessionNode(r.get("cs").properties)); } /** * Delete a compacted session */ export async function deleteCompactedSession(uuid: string): Promise { const query = ` MATCH (cs:CompactedSession {uuid: $uuid}) DETACH DELETE cs `; await runQuery(query, { uuid }); } /** * Get compaction statistics for a user */ export async function getCompactionStats(userId: string): Promise<{ totalCompacts: number; totalEpisodes: number; averageCompressionRatio: number; mostRecentCompaction: Date | null; }> { const query = ` MATCH (cs:CompactedSession {userId: $userId}) RETURN count(cs) as totalCompacts, sum(cs.episodeCount) as totalEpisodes, avg(cs.compressionRatio) as avgCompressionRatio, max(cs.endTime) as mostRecent `; const result = await runQuery(query, { userId }); if (result.length === 0) { return { totalCompacts: 0, totalEpisodes: 0, averageCompressionRatio: 0, mostRecentCompaction: null, }; } const stats = result[0]; return { totalCompacts: stats.get("totalCompacts")?.toNumber() || 0, totalEpisodes: stats.get("totalEpisodes")?.toNumber() || 0, averageCompressionRatio: stats.get("avgCompressionRatio") || 0, mostRecentCompaction: stats.get("mostRecent") ? new Date(stats.get("mostRecent")) : null, }; } /** * Get all episodes for a session */ export async function getSessionEpisodes( sessionId: string, userId: string, afterTime?: Date ): Promise { const query = ` MATCH (e:Episode {sessionId: $sessionId, userId: $userId}) ${afterTime ? "WHERE e.createdAt > datetime($afterTime)" : ""} RETURN e ORDER BY e.createdAt ASC `; const result = await runQuery(query, { sessionId, userId, afterTime: afterTime?.toISOString(), }); return result.map((r) => r.get("e").properties); } /** * Get episode count for a session */ export async function getSessionEpisodeCount( sessionId: string, userId: string, afterTime?: Date ): Promise { const episodes = await getSessionEpisodes(sessionId, userId, afterTime); return episodes.length; } /** * Helper to parse raw compact node from Neo4j */ function parseCompactedSessionNode(raw: any): CompactedSessionNode { return { uuid: raw.uuid, sessionId: raw.sessionId, summary: raw.summary, summaryEmbedding: raw.summaryEmbedding || [], episodeCount: raw.episodeCount || 0, startTime: new Date(raw.startTime), endTime: new Date(raw.endTime), createdAt: new Date(raw.createdAt), updatedAt: raw.updatedAt ? new Date(raw.updatedAt) : undefined, confidence: raw.confidence || 0, userId: raw.userId, source: raw.source, compressionRatio: raw.compressionRatio || 1, metadata: typeof raw.metadata === "string" ? JSON.parse(raw.metadata) : raw.metadata || {}, }; }