This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// The one AI setting of this app: which model the platform's AI gateway is asked for. The gateway's
|
||||
// token and URL are the deployment contract (@conusai-kit/platform); the model is a choice, so it has
|
||||
// a default that works and a variable that changes it, per environment, without a rebuild.
|
||||
|
||||
/** A model the ConusAI AI gateway serves; the extraction prompt of `modules/documents` is written against this family. */
|
||||
export const DEFAULT_AI_MODEL = "anthropic/claude-sonnet-5"
|
||||
|
||||
export const AI_MODEL_VAR = "AI_MODEL"
|
||||
|
||||
/** The model id the app asks the gateway for: `AI_MODEL` when set, otherwise the default. */
|
||||
export function aiModelOf(env: Record<string, string | undefined> = process.env): string {
|
||||
const chosen = env[AI_MODEL_VAR]?.trim()
|
||||
return chosen ? chosen : DEFAULT_AI_MODEL
|
||||
}
|
||||
|
||||
// The knowledge module's deployment choices: which search backend serves it, which embeddings
|
||||
// model the vector backend asks for, and the vector size the store is built with.
|
||||
//
|
||||
// KNOWLEDGE_BACKEND "vectors" (default): chunks and queries are embedded through the AI
|
||||
// gateway and stored in the app's vector store (KNOWLEDGE_VECTOR_STORE
|
||||
// below: a linked Qdrant, or pgvector when chosen explicitly).
|
||||
// "platform": the platform's managed documents-index service does the
|
||||
// indexing and the retrieval.
|
||||
// EMBEDDING_MODEL the gateway's embeddings model for the vectors backend (required when the
|
||||
// backend is vectors; AI_EMBEDDING_MODEL overrides).
|
||||
// VECTOR_DIMENSIONS the embedding size the vector store is built with; the pgvector column and
|
||||
// the Qdrant collection both take it (default 1536).
|
||||
|
||||
import { z } from "zod"
|
||||
|
||||
export const KNOWLEDGE_BACKEND_VAR = "KNOWLEDGE_BACKEND"
|
||||
|
||||
export const KNOWLEDGE_BACKENDS = ["vectors", "platform"] as const
|
||||
export type KnowledgeBackendChoice = (typeof KNOWLEDGE_BACKENDS)[number]
|
||||
|
||||
export const knowledgeBackendSchema = z.enum(KNOWLEDGE_BACKENDS)
|
||||
|
||||
/** Which search backend this deployment binds; "vectors" unless the operator says otherwise. */
|
||||
export function knowledgeBackendOf(env: Record<string, string | undefined> = process.env): KnowledgeBackendChoice {
|
||||
const raw = env[KNOWLEDGE_BACKEND_VAR]?.trim().toLowerCase()
|
||||
if (raw === undefined || raw === "") return "vectors"
|
||||
const parsed = knowledgeBackendSchema.safeParse(raw)
|
||||
if (!parsed.success) {
|
||||
throw new Error(`${KNOWLEDGE_BACKEND_VAR} must be one of ${KNOWLEDGE_BACKENDS.join(", ")}`)
|
||||
}
|
||||
return parsed.data
|
||||
}
|
||||
|
||||
export const EMBEDDING_MODEL_VAR = "AI_EMBEDDING_MODEL"
|
||||
|
||||
/** The embeddings model the vector backend asks the gateway for. */
|
||||
export function embeddingModelOf(env: Record<string, string | undefined> = process.env): string | undefined {
|
||||
const chosen = env[EMBEDDING_MODEL_VAR]?.trim()
|
||||
return chosen ? chosen : undefined
|
||||
}
|
||||
|
||||
export const VECTOR_DIMENSIONS_VAR = "VECTOR_DIMENSIONS"
|
||||
|
||||
export const DEFAULT_VECTOR_DIMENSIONS = 1536
|
||||
|
||||
/** The vector size the store is built with; must match the embeddings model's output. */
|
||||
export function vectorDimensionsOf(env: Record<string, string | undefined> = process.env): number {
|
||||
const raw = env[VECTOR_DIMENSIONS_VAR]?.trim()
|
||||
if (raw === undefined || raw === "") return DEFAULT_VECTOR_DIMENSIONS
|
||||
const value = Number(raw)
|
||||
if (!Number.isInteger(value) || value < 1 || value > 16_000) {
|
||||
throw new Error(`${VECTOR_DIMENSIONS_VAR} must be an integer between 1 and 16000`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// Where the vectors backend keeps its vectors (decision 2026-10-25, managed-AI closeout):
|
||||
//
|
||||
// KNOWLEDGE_VECTOR_STORE unset (default): Qdrant when a Qdrant service is linked (QDRANT_URL),
|
||||
// otherwise NO store — the knowledge module reports itself unavailable
|
||||
// with the reason below, and the migrate job creates nothing.
|
||||
// "pgvector": the app's own Postgres, opted into explicitly. The
|
||||
// platform's default managed Postgres image does not ship the extension;
|
||||
// pgvector needs a Postgres created from the pgvector image.
|
||||
// "qdrant": Qdrant, required — unavailable until QDRANT_URL is set.
|
||||
//
|
||||
// pgvector used to be the silent default whenever no Qdrant was linked, so a fresh app on the
|
||||
// default managed Postgres failed its migrate job (`extension "vector" is not available`) before
|
||||
// anyone had asked for document AI. Enabling document AI links a managed Qdrant, which is the
|
||||
// product path; pgvector stays a deliberate choice for databases that offer it.
|
||||
|
||||
export const KNOWLEDGE_VECTOR_STORE_VAR = "KNOWLEDGE_VECTOR_STORE"
|
||||
|
||||
export const VECTOR_STORES = ["qdrant", "pgvector"] as const
|
||||
export type VectorStoreChoice = (typeof VECTOR_STORES)[number]
|
||||
|
||||
const vectorStoreSchema = z.enum(VECTOR_STORES)
|
||||
|
||||
/** Why the vectors backend has no store, for the knowledge status a person reads. */
|
||||
export const NO_VECTOR_STORE_REASON =
|
||||
"no vector store is configured: enable document AI to link a managed Qdrant, set KNOWLEDGE_VECTOR_STORE=pgvector on a Postgres that has the pgvector extension, or set KNOWLEDGE_BACKEND=platform"
|
||||
|
||||
/**
|
||||
* The vector store this deployment binds, or null when the vectors backend has none (or the
|
||||
* backend is the platform's). Explicit pgvector only: never inferred from a missing Qdrant.
|
||||
*/
|
||||
export function vectorStoreOf(environment: Record<string, string | undefined> = process.env): VectorStoreChoice | null {
|
||||
if (knowledgeBackendOf(environment) !== "vectors") return null
|
||||
const raw = environment[KNOWLEDGE_VECTOR_STORE_VAR]?.trim().toLowerCase()
|
||||
const linkedQdrant = Boolean(environment.QDRANT_URL?.trim())
|
||||
if (raw === undefined || raw === "") return linkedQdrant ? "qdrant" : null
|
||||
const parsed = vectorStoreSchema.safeParse(raw)
|
||||
if (!parsed.success) {
|
||||
throw new Error(`${KNOWLEDGE_VECTOR_STORE_VAR} must be one of ${VECTOR_STORES.join(", ")}`)
|
||||
}
|
||||
if (parsed.data === "qdrant" && !linkedQdrant) return null
|
||||
return parsed.data
|
||||
}
|
||||
|
||||
/**
|
||||
* The runtime-built migration that creates the pgvector table, for a deployment that chose the
|
||||
* pgvector store. The migrate job applies it; the web readiness probe expects exactly its name, so
|
||||
* the decision lives here, in the one place that reads the same environment.
|
||||
*/
|
||||
export function vectorStoreMigrationName(environment: Record<string, string | undefined> = process.env): string | null {
|
||||
return vectorStoreOf(environment) === "pgvector" ? "0006_vectors" : null
|
||||
}
|
||||
Reference in New Issue
Block a user