project-nomad/admin/constants/kb_collections.ts

60 lines
2.4 KiB
TypeScript

/**
* Curated starter tags shown in the collection picker. These are just
* suggested defaults — the actual set of usable tags is open-ended, since
* `collection` is a free-form string and getKnowledgeCollections() returns
* whatever's actually in use (see RagService). Kept general-purpose rather
* than survival-specific so NOMAD's Knowledge Base reads well for home-lab,
* reference, and everyday use too.
*/
export const KB_COLLECTIONS = [
'recipes',
'diy',
'health',
'technology',
'finance',
'travel',
'hobbies',
'reference',
'survival',
'energy',
] as const
export type KbCollection = (typeof KB_COLLECTIONS)[number]
/**
* Reserved collection tag for the developer evaluation corpus (`ace eval:*`).
*
* The eval fixtures live in the same Qdrant collection as everything else —
* NOMAD "collections" are a payload tag, not separate Qdrant collections — so
* this tag is what keeps them out of the user's Knowledge Base UI and out of
* ordinary chat retrieval. Every read path that enumerates user content filters
* it out, and `sanitizeCollectionName` refuses to mint it, so a user cannot
* create a colliding tag by accident.
*
* The leading/trailing underscores are deliberate: they make the tag obviously
* internal if it ever does surface in a log or a raw Qdrant query.
*/
export const KB_EVAL_COLLECTION = '__nomad_eval__'
/** Hard cap on a user-created tag's length, enforced client- and server-side. */
export const KB_COLLECTION_NAME_MAX_LENGTH = 40
/**
* Normalize a user-entered collection name: trim whitespace, lowercase, cap
* length. Returns null for empty/whitespace-only input, meaning
* "uncategorized". Lowercasing is what makes de-dupe work — "Medical" and
* "medical" normalize to the same tag rather than forking into two, so this
* must run on every write path (upload, reassignment, rename) both
* client-side for instant feedback and server-side as the actual guarantee.
*/
export function sanitizeCollectionName(raw: string | null | undefined): string | null {
if (!raw) return null
const trimmed = raw.trim().toLowerCase()
if (!trimmed) return null
// The eval tag is reserved. Treating a collision as "uncategorized" rather
// than throwing keeps this a pure normalizer, and the user's documents stay
// visible in the KB instead of vanishing into a hidden internal collection.
if (trimmed === KB_EVAL_COLLECTION) return null
return trimmed.slice(0, KB_COLLECTION_NAME_MAX_LENGTH)
}