feat(palace): add user-isolated repository with CRUD, search, dedup, prune, encryption
This commit is contained in:
parent
38006af1a3
commit
44d8867aa5
540
backend/src/modules/palace/repository.ts
Normal file
540
backend/src/modules/palace/repository.ts
Normal file
@ -0,0 +1,540 @@
|
|||||||
|
import { getCollection } from '../../lib/datastore.js';
|
||||||
|
import { getEncryptor } from '../../lib/field-encrypt.js';
|
||||||
|
import { isEncryptedField, type EncryptedField } from '@bytelyst/field-encrypt';
|
||||||
|
import { embedText } from '../../lib/embeddings.js';
|
||||||
|
import { config } from '../../lib/config.js';
|
||||||
|
import { cosineSimilarity, topKByCosine, isContentDuplicate, computeDecayedRelevance } from '@bytelyst/palace';
|
||||||
|
import type { FilterMap } from '@bytelyst/datastore';
|
||||||
|
import type {
|
||||||
|
PalaceWingDoc,
|
||||||
|
PalaceRoomDoc,
|
||||||
|
PalaceMemoryDoc,
|
||||||
|
PalaceTunnelDoc,
|
||||||
|
PalaceKGTripleDoc,
|
||||||
|
PalaceDiaryDoc,
|
||||||
|
HallType,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
const ENCRYPT_CONTEXT = 'palace-memory';
|
||||||
|
|
||||||
|
// ── Collection accessors ────────────────────────────────────────────
|
||||||
|
|
||||||
|
function wingsCollection() {
|
||||||
|
return getCollection<PalaceWingDoc>('palace_wings', '/userId');
|
||||||
|
}
|
||||||
|
function roomsCollection() {
|
||||||
|
return getCollection<PalaceRoomDoc>('palace_rooms', '/userId');
|
||||||
|
}
|
||||||
|
function memoriesCollection() {
|
||||||
|
return getCollection<PalaceMemoryDoc>('palace_memories', '/userId');
|
||||||
|
}
|
||||||
|
function tunnelsCollection() {
|
||||||
|
return getCollection<PalaceTunnelDoc>('palace_tunnels', '/userId');
|
||||||
|
}
|
||||||
|
function kgCollection() {
|
||||||
|
return getCollection<PalaceKGTripleDoc>('palace_kg', '/userId');
|
||||||
|
}
|
||||||
|
function diariesCollection() {
|
||||||
|
return getCollection<PalaceDiaryDoc>('palace_diaries', '/userId');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Field Encryption ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function encryptMemoryContent(doc: PalaceMemoryDoc): Promise<PalaceMemoryDoc> {
|
||||||
|
const enc = getEncryptor();
|
||||||
|
const ctx = { userId: doc.userId, context: ENCRYPT_CONTEXT };
|
||||||
|
const encrypted = await enc.encrypt(doc.content, ctx);
|
||||||
|
return { ...doc, content: encrypted as unknown as string };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decryptMemoryContent(doc: PalaceMemoryDoc): Promise<PalaceMemoryDoc> {
|
||||||
|
const enc = getEncryptor();
|
||||||
|
const ctx = { userId: doc.userId, context: ENCRYPT_CONTEXT };
|
||||||
|
let content = doc.content;
|
||||||
|
if (isEncryptedField(content)) {
|
||||||
|
content = await enc.decrypt(content as unknown as EncryptedField, ctx);
|
||||||
|
}
|
||||||
|
return { ...doc, content };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decryptMemoryBatch(docs: PalaceMemoryDoc[]): Promise<PalaceMemoryDoc[]> {
|
||||||
|
return Promise.all(docs.map(decryptMemoryContent));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Wings ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function ensureWing(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
sourceWorkspaceId: string,
|
||||||
|
name: string,
|
||||||
|
): Promise<PalaceWingDoc> {
|
||||||
|
const existing = await wingsCollection().findMany({
|
||||||
|
filter: { userId, productId, sourceWorkspaceId },
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
if (existing.length > 0) return existing[0];
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const wing: PalaceWingDoc = {
|
||||||
|
id: `wing-${sourceWorkspaceId}`,
|
||||||
|
productId,
|
||||||
|
userId,
|
||||||
|
name,
|
||||||
|
sourceWorkspaceId,
|
||||||
|
memoryCount: 0,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
return wingsCollection().create(wing);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWing(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
wingId: string,
|
||||||
|
): Promise<PalaceWingDoc | null> {
|
||||||
|
const doc = await wingsCollection().findById(wingId, userId);
|
||||||
|
if (!doc || doc.productId !== productId) return null;
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listWings(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
): Promise<PalaceWingDoc[]> {
|
||||||
|
return wingsCollection().findMany({
|
||||||
|
filter: { userId, productId },
|
||||||
|
sort: { updatedAt: -1 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteWing(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
wingId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const wing = await getWing(userId, productId, wingId);
|
||||||
|
if (!wing) return;
|
||||||
|
|
||||||
|
// Cascade delete: rooms, memories, tunnels, KG, diaries
|
||||||
|
const rooms = await roomsCollection().findMany({ filter: { userId, productId, wingId } });
|
||||||
|
for (const room of rooms) {
|
||||||
|
await roomsCollection().delete(room.id, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const memories = await memoriesCollection().findMany({ filter: { userId, productId, wingId } });
|
||||||
|
for (const mem of memories) {
|
||||||
|
await memoriesCollection().delete(mem.id, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tunnels = await tunnelsCollection().findMany({ filter: { userId, productId } });
|
||||||
|
for (const tunnel of tunnels) {
|
||||||
|
if (tunnel.fromWingId === wingId || tunnel.toWingId === wingId) {
|
||||||
|
await tunnelsCollection().delete(tunnel.id, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const triples = await kgCollection().findMany({ filter: { userId, productId, wingId } });
|
||||||
|
for (const triple of triples) {
|
||||||
|
await kgCollection().delete(triple.id, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const diaries = await diariesCollection().findMany({ filter: { userId, productId, wingId } });
|
||||||
|
for (const diary of diaries) {
|
||||||
|
await diariesCollection().delete(diary.id, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await wingsCollection().delete(wingId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Rooms ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function ensureRoom(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
wingId: string,
|
||||||
|
name: string,
|
||||||
|
description?: string,
|
||||||
|
): Promise<PalaceRoomDoc> {
|
||||||
|
const existing = await roomsCollection().findMany({
|
||||||
|
filter: { userId, productId, wingId, name },
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
if (existing.length > 0) return existing[0];
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const room: PalaceRoomDoc = {
|
||||||
|
id: `room-${wingId}-${name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`,
|
||||||
|
productId,
|
||||||
|
userId,
|
||||||
|
wingId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
memoryCount: 0,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
return roomsCollection().create(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listRooms(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
wingId: string,
|
||||||
|
): Promise<PalaceRoomDoc[]> {
|
||||||
|
return roomsCollection().findMany({
|
||||||
|
filter: { userId, productId, wingId },
|
||||||
|
sort: { createdAt: -1 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Memories ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function storeMemory(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
wingId: string,
|
||||||
|
roomId: string,
|
||||||
|
hall: HallType,
|
||||||
|
content: string,
|
||||||
|
sourceNoteId?: string,
|
||||||
|
embedding?: number[] | null,
|
||||||
|
): Promise<PalaceMemoryDoc> {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const doc: PalaceMemoryDoc = {
|
||||||
|
id: `mem-${crypto.randomUUID()}`,
|
||||||
|
productId,
|
||||||
|
userId,
|
||||||
|
wingId,
|
||||||
|
roomId,
|
||||||
|
hall,
|
||||||
|
content,
|
||||||
|
relevance: 1.0,
|
||||||
|
embedding: embedding ?? undefined,
|
||||||
|
sourceNoteId,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
const encrypted = await encryptMemoryContent(doc);
|
||||||
|
const created = await memoriesCollection().create(encrypted);
|
||||||
|
|
||||||
|
// Increment memory counts
|
||||||
|
await incrementMemoryCount(userId, wingId, roomId);
|
||||||
|
|
||||||
|
return decryptMemoryContent(created);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function incrementMemoryCount(userId: string, wingId: string, roomId: string): Promise<void> {
|
||||||
|
const wing = await wingsCollection().findById(wingId, userId);
|
||||||
|
if (wing) {
|
||||||
|
await wingsCollection().update(wingId, userId, {
|
||||||
|
memoryCount: (wing.memoryCount || 0) + 1,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const room = await roomsCollection().findById(roomId, userId);
|
||||||
|
if (room) {
|
||||||
|
await roomsCollection().update(roomId, userId, {
|
||||||
|
memoryCount: (room.memoryCount || 0) + 1,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMemory(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
memoryId: string,
|
||||||
|
): Promise<PalaceMemoryDoc | null> {
|
||||||
|
const doc = await memoriesCollection().findById(memoryId, userId);
|
||||||
|
if (!doc || doc.productId !== productId) return null;
|
||||||
|
return decryptMemoryContent(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteMemory(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
memoryId: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const doc = await memoriesCollection().findById(memoryId, userId);
|
||||||
|
if (!doc || doc.productId !== productId) return false;
|
||||||
|
await memoriesCollection().delete(memoryId, userId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listMemories(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
opts: { wingId?: string; roomId?: string; hall?: HallType; limit?: number },
|
||||||
|
): Promise<PalaceMemoryDoc[]> {
|
||||||
|
const filter: FilterMap = { userId, productId };
|
||||||
|
if (opts.wingId) filter.wingId = opts.wingId;
|
||||||
|
if (opts.roomId) filter.roomId = opts.roomId;
|
||||||
|
if (opts.hall) filter.hall = opts.hall;
|
||||||
|
|
||||||
|
const docs = await memoriesCollection().findMany({
|
||||||
|
filter,
|
||||||
|
sort: { updatedAt: -1 },
|
||||||
|
limit: opts.limit ?? 50,
|
||||||
|
});
|
||||||
|
return decryptMemoryBatch(docs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Search ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function searchSemantic(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
_query: string,
|
||||||
|
embedding: number[],
|
||||||
|
wingId?: string,
|
||||||
|
limit = 10,
|
||||||
|
): Promise<PalaceMemoryDoc[]> {
|
||||||
|
const filter: FilterMap = { userId, productId };
|
||||||
|
if (wingId) filter.wingId = wingId;
|
||||||
|
|
||||||
|
const candidates = await memoriesCollection().findMany({
|
||||||
|
filter,
|
||||||
|
sort: { updatedAt: -1 },
|
||||||
|
limit: limit * 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
const decrypted = await decryptMemoryBatch(candidates);
|
||||||
|
const ranked = topKByCosine(
|
||||||
|
embedding,
|
||||||
|
decrypted.filter(m => m.embedding && m.embedding.length > 0),
|
||||||
|
(m: PalaceMemoryDoc) => m.embedding,
|
||||||
|
limit,
|
||||||
|
);
|
||||||
|
|
||||||
|
return ranked.map(r => r.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchText(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
query: string,
|
||||||
|
wingId?: string,
|
||||||
|
limit = 10,
|
||||||
|
): Promise<PalaceMemoryDoc[]> {
|
||||||
|
const filter: FilterMap = { userId, productId };
|
||||||
|
if (wingId) filter.wingId = wingId;
|
||||||
|
|
||||||
|
// Text search: fetch all, decrypt, filter in-memory
|
||||||
|
const candidates = await memoriesCollection().findMany({
|
||||||
|
filter,
|
||||||
|
sort: { updatedAt: -1 },
|
||||||
|
limit: limit * 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const decrypted = await decryptMemoryBatch(candidates);
|
||||||
|
const lowerQ = query.toLowerCase();
|
||||||
|
return decrypted
|
||||||
|
.filter(m => m.content.toLowerCase().includes(lowerQ))
|
||||||
|
.slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchHybrid(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
query: string,
|
||||||
|
embedding: number[],
|
||||||
|
wingId?: string,
|
||||||
|
limit = 10,
|
||||||
|
): Promise<PalaceMemoryDoc[]> {
|
||||||
|
const filter: FilterMap = { userId, productId };
|
||||||
|
if (wingId) filter.wingId = wingId;
|
||||||
|
|
||||||
|
// Fetch candidates
|
||||||
|
const candidates = await memoriesCollection().findMany({
|
||||||
|
filter,
|
||||||
|
sort: { updatedAt: -1 },
|
||||||
|
limit: limit * 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const decrypted = await decryptMemoryBatch(candidates);
|
||||||
|
const lowerQ = query.toLowerCase();
|
||||||
|
|
||||||
|
// Text filter first
|
||||||
|
const textMatches = decrypted.filter(m => m.content.toLowerCase().includes(lowerQ));
|
||||||
|
|
||||||
|
// Re-rank by cosine similarity
|
||||||
|
const withEmbeddings = textMatches.filter(m => m.embedding && m.embedding.length > 0);
|
||||||
|
if (withEmbeddings.length === 0) return textMatches.slice(0, limit);
|
||||||
|
|
||||||
|
const ranked = topKByCosine(embedding, withEmbeddings, (m: PalaceMemoryDoc) => m.embedding, limit);
|
||||||
|
return ranked.map(r => r.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Wing Summary ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getWingSummary(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
wingId: string,
|
||||||
|
): Promise<{ wing: PalaceWingDoc | null; rooms: PalaceRoomDoc[]; totalMemories: number }> {
|
||||||
|
const wing = await getWing(userId, productId, wingId);
|
||||||
|
if (!wing) return { wing: null, rooms: [], totalMemories: 0 };
|
||||||
|
|
||||||
|
const rooms = await listRooms(userId, productId, wingId);
|
||||||
|
const totalMemories = await memoriesCollection().count({ userId, productId, wingId });
|
||||||
|
|
||||||
|
return { wing, rooms, totalMemories };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Deduplication ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function isNearDuplicate(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
roomId: string,
|
||||||
|
hall: HallType,
|
||||||
|
content: string,
|
||||||
|
embedding: number[] | null,
|
||||||
|
threshold?: number,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const dupThreshold = threshold ?? config.PALACE_DEDUP_THRESHOLD;
|
||||||
|
|
||||||
|
// 1. Exact match (cheap)
|
||||||
|
const filter: FilterMap = { userId, productId, roomId, hall };
|
||||||
|
const candidates = await memoriesCollection().findMany({
|
||||||
|
filter,
|
||||||
|
sort: { createdAt: -1 },
|
||||||
|
limit: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
const decrypted = await decryptMemoryBatch(candidates);
|
||||||
|
|
||||||
|
// Check exact
|
||||||
|
if (decrypted.some(c => c.content === content)) return true;
|
||||||
|
|
||||||
|
// 2. Semantic dedup via shared @bytelyst/palace helper
|
||||||
|
if (embedding) {
|
||||||
|
const candidateEmbeddings = decrypted
|
||||||
|
.map(c => c.embedding)
|
||||||
|
.filter((e): e is number[] => !!e && e.length > 0);
|
||||||
|
|
||||||
|
return isContentDuplicate(embedding, candidateEmbeddings, dupThreshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Maintenance ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function pruneOldMemories(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
wingId: string | undefined,
|
||||||
|
olderThanDays: number,
|
||||||
|
minRelevance: number,
|
||||||
|
): Promise<number> {
|
||||||
|
const filter: FilterMap = { userId, productId };
|
||||||
|
if (wingId) filter.wingId = wingId;
|
||||||
|
|
||||||
|
const all = await memoriesCollection().findMany({ filter, limit: 1000 });
|
||||||
|
const cutoff = new Date(Date.now() - olderThanDays * 86_400_000).toISOString();
|
||||||
|
let deleted = 0;
|
||||||
|
|
||||||
|
for (const mem of all) {
|
||||||
|
if (mem.createdAt < cutoff && mem.relevance < minRelevance) {
|
||||||
|
await memoriesCollection().delete(mem.id, userId);
|
||||||
|
deleted++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decayRelevance(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
halfLifeDays?: number,
|
||||||
|
): Promise<number> {
|
||||||
|
const halfLife = halfLifeDays ?? config.PALACE_RELEVANCE_HALF_LIFE_DAYS;
|
||||||
|
const filter: FilterMap = { userId, productId };
|
||||||
|
|
||||||
|
const all = await memoriesCollection().findMany({ filter, limit: 5000 });
|
||||||
|
let updated = 0;
|
||||||
|
|
||||||
|
for (const mem of all) {
|
||||||
|
const decayed = computeDecayedRelevance(mem.relevance, mem.createdAt, halfLife);
|
||||||
|
if (Math.abs(decayed - mem.relevance) > 0.001) {
|
||||||
|
await memoriesCollection().update(mem.id, userId, {
|
||||||
|
relevance: decayed,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function backfillEmbeddings(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
): Promise<number> {
|
||||||
|
const filter: FilterMap = { userId, productId };
|
||||||
|
const all = await memoriesCollection().findMany({ filter, limit: 500 });
|
||||||
|
const decrypted = await decryptMemoryBatch(all);
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (const mem of decrypted) {
|
||||||
|
if (!mem.embedding || mem.embedding.length === 0) {
|
||||||
|
const emb = await embedText(mem.content);
|
||||||
|
if (emb) {
|
||||||
|
await memoriesCollection().update(mem.id, userId, {
|
||||||
|
embedding: emb,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Stats ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getPalaceStats(
|
||||||
|
userId: string,
|
||||||
|
productId: string,
|
||||||
|
): Promise<{
|
||||||
|
wings: number;
|
||||||
|
rooms: number;
|
||||||
|
memories: number;
|
||||||
|
kgTriples: number;
|
||||||
|
tunnels: number;
|
||||||
|
diaries: number;
|
||||||
|
}> {
|
||||||
|
const [wings, rooms, memories, kgTriples, tunnels, diaries] = await Promise.all([
|
||||||
|
wingsCollection().count({ userId, productId }),
|
||||||
|
roomsCollection().count({ userId, productId }),
|
||||||
|
memoriesCollection().count({ userId, productId }),
|
||||||
|
kgCollection().count({ userId, productId }),
|
||||||
|
tunnelsCollection().count({ userId, productId }),
|
||||||
|
diariesCollection().count({ userId, productId }),
|
||||||
|
]);
|
||||||
|
return { wings, rooms, memories, kgTriples, tunnels, diaries };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function healthCheck(): Promise<{ cosmos: boolean; llm: boolean }> {
|
||||||
|
let cosmos = false;
|
||||||
|
let llm = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await wingsCollection().count({});
|
||||||
|
cosmos = true;
|
||||||
|
} catch { /* cosmos unavailable */ }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const emb = await embedText('health check');
|
||||||
|
llm = emb !== null;
|
||||||
|
} catch { /* llm unavailable */ }
|
||||||
|
|
||||||
|
return { cosmos, llm };
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user