This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
// The constants of the knowledge module: its feature flag, jobs, events, audit actions and the
|
||||
// indexable content types.
|
||||
|
||||
/** The module flag: knowledge retrieval and indexing, off until an administrator enables it. */
|
||||
export const KNOWLEDGE_FLAG = "knowledge.enabled"
|
||||
|
||||
export const KNOWLEDGE_FLAG_DEFAULTS = {
|
||||
[KNOWLEDGE_FLAG]: false,
|
||||
} as const
|
||||
|
||||
/** The worker job that indexes one document's chunks into the vector store. */
|
||||
export const INDEX_JOB = "knowledge.index"
|
||||
|
||||
/** The worker job that removes one document's chunks from the vector store. */
|
||||
export const REMOVE_JOB = "knowledge.remove"
|
||||
|
||||
export const COLLECTION_CREATED = "knowledge.collection.created"
|
||||
export const DOCUMENT_INDEX_REQUESTED = "knowledge.document.index_requested"
|
||||
export const DOCUMENT_REMOVE_REQUESTED = "knowledge.document.remove_requested"
|
||||
export const DOCUMENT_AI_ENABLED = "knowledge.settings.document_ai_enabled"
|
||||
export const DOCUMENT_AI_DISABLED = "knowledge.settings.document_ai_disabled"
|
||||
|
||||
/**
|
||||
* What the indexer reads as text. PDFs and scans are the documents module's worked example; this
|
||||
* module indexes text-shaped files, and refuses anything else at the door with a named field.
|
||||
*/
|
||||
export const INDEXABLE_CONTENT_TYPES = ["text/plain", "text/markdown", "application/json", "text/csv"] as const
|
||||
|
||||
/** A search query's ceiling; a retrieval question is a sentence, not a document. */
|
||||
export const MAX_QUERY_LENGTH = 500
|
||||
|
||||
/** What one search returns at most, however much the caller asks (the retrieval limit contract). */
|
||||
export const MAX_SEARCH_RESULTS = 25
|
||||
|
||||
/** An indexed file's ceiling: one megabyte of text is already far past useful chunking. */
|
||||
export const MAX_INDEX_BYTES = 1_048_576
|
||||
@@ -0,0 +1,68 @@
|
||||
// The tables of the knowledge module. Three.
|
||||
//
|
||||
// `knowledge_collection` is one named collection of documents, project-owned; the collection name
|
||||
// is the slug procedures take (a procedure's input carries slugs, never ids). `knowledge_document`
|
||||
// is one file's indexing state in one collection — the row the UI's progress states read, and the
|
||||
// idempotency key of the index job (a file is indexed into a collection once, and re-indexing the
|
||||
// same pair resumes the one row). `knowledge_config` is the organization's enablement state: the
|
||||
// durable answer to "is document AI on", which backend serves it and where the managed vector
|
||||
// service was linked. No secret ever lands here: the Qdrant API key lives in the deployment
|
||||
// environment, not in a tenant row.
|
||||
|
||||
import { defineEntity, idSchema, instantSchema } from "@conusai-kit/data"
|
||||
import { z } from "zod"
|
||||
import { DOCUMENT_STATUSES } from "./statuses.js"
|
||||
|
||||
export const collections = defineEntity({
|
||||
name: "collection",
|
||||
table: "knowledge_collection",
|
||||
tenancy: "project",
|
||||
fields: {
|
||||
name: z.string().min(1).max(100),
|
||||
createdBy: z.string().min(1).max(128),
|
||||
},
|
||||
unique: [["name"]],
|
||||
})
|
||||
|
||||
export const documents = defineEntity({
|
||||
name: "document",
|
||||
table: "knowledge_document",
|
||||
tenancy: "project",
|
||||
fields: {
|
||||
collectionName: z.string().min(1).max(100),
|
||||
/** The row of the files module this document indexes: an opaque id, never a foreign key. */
|
||||
fileId: idSchema,
|
||||
filename: z.string().min(1).max(255),
|
||||
status: z.enum(DOCUMENT_STATUSES),
|
||||
/** A stable failure code when the indexing failed; null otherwise. */
|
||||
error: z.string().max(200).nullable(),
|
||||
indexedAt: z.string().nullable(),
|
||||
requestedBy: z.string().min(1).max(128),
|
||||
},
|
||||
// One row per (collection, file): a re-index resumes the same row instead of duplicating it.
|
||||
unique: [["collectionName", "fileId"]],
|
||||
indexes: [["collectionName"]],
|
||||
})
|
||||
|
||||
export const config = defineEntity({
|
||||
name: "config",
|
||||
table: "knowledge_config",
|
||||
tenancy: "org",
|
||||
fields: {
|
||||
enabled: z.boolean(),
|
||||
/** Which search backend serves this organization: the local vector store or the managed service. */
|
||||
backend: z.enum(["vectors", "platform"]),
|
||||
/** Where the vector backing came from: an operator-configured endpoint or a managed link. */
|
||||
source: z.enum(["operator", "managed"]),
|
||||
/** The linked managed service's id, when source is managed. */
|
||||
serviceId: z.string().max(128).nullable(),
|
||||
/** The operator- or link-configured endpoint, when there is one to show. Never a key. */
|
||||
url: z.string().max(500).nullable(),
|
||||
updatedBy: z.string().min(1).max(128),
|
||||
/** When the configuration last changed; the managed updated_at is the row's own bookkeeping. */
|
||||
configuredAt: instantSchema,
|
||||
},
|
||||
})
|
||||
|
||||
/** Every table this module owns, in the order a migration creates them. */
|
||||
export const KNOWLEDGE_ENTITIES = Object.freeze([collections, documents, config])
|
||||
@@ -0,0 +1,36 @@
|
||||
// The events of the knowledge module. A write procedure publishes the request; the module's own
|
||||
// subscriber (running in the worker's outbox relay) enqueues the job, which is how a web request
|
||||
// never waits on chunking, embeddings or a vector store.
|
||||
|
||||
import { defineEvent } from "@conusai-kit/core"
|
||||
import { z } from "zod"
|
||||
|
||||
const inProject = {
|
||||
orgId: z.string(),
|
||||
workspaceId: z.string(),
|
||||
projectId: z.string(),
|
||||
}
|
||||
|
||||
/** A document was asked to be indexed: the subscriber enqueues the index job. */
|
||||
export const documentIndexRequested = defineEvent(
|
||||
"knowledge.documentIndexRequested",
|
||||
z.object({
|
||||
...inProject,
|
||||
documentId: z.string(),
|
||||
collectionName: z.string(),
|
||||
fileId: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
/** A document was asked to be removed: the subscriber enqueues the remove job. */
|
||||
export const documentRemoveRequested = defineEvent(
|
||||
"knowledge.documentRemoveRequested",
|
||||
z.object({
|
||||
...inProject,
|
||||
documentId: z.string(),
|
||||
collectionName: z.string(),
|
||||
fileId: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
export const KNOWLEDGE_EVENTS = [documentIndexRequested, documentRemoveRequested]
|
||||
@@ -0,0 +1,45 @@
|
||||
// The public API of the knowledge module: what a page, another module and the OpenAPI document may
|
||||
// import. Nothing here reaches core/, adapters/ or a driver.
|
||||
|
||||
export {
|
||||
COLLECTION_CREATED,
|
||||
DOCUMENT_AI_DISABLED,
|
||||
DOCUMENT_AI_ENABLED,
|
||||
DOCUMENT_INDEX_REQUESTED,
|
||||
DOCUMENT_REMOVE_REQUESTED,
|
||||
INDEX_JOB,
|
||||
INDEXABLE_CONTENT_TYPES,
|
||||
KNOWLEDGE_FLAG,
|
||||
KNOWLEDGE_FLAG_DEFAULTS,
|
||||
MAX_INDEX_BYTES,
|
||||
MAX_QUERY_LENGTH,
|
||||
MAX_SEARCH_RESULTS,
|
||||
REMOVE_JOB,
|
||||
} from "./constants.js"
|
||||
export { collections, config, documents, KNOWLEDGE_ENTITIES } from "./entities.js"
|
||||
export { documentIndexRequested, documentRemoveRequested, KNOWLEDGE_EVENTS } from "./events.js"
|
||||
export type { CollectionView, DocumentView, SearchResult } from "./procedures.js"
|
||||
export {
|
||||
collectionCreate,
|
||||
collectionList,
|
||||
collectionName,
|
||||
collectionViewSchema,
|
||||
documentIndex,
|
||||
documentList,
|
||||
documentRemove,
|
||||
documentViewSchema,
|
||||
KNOWLEDGE_PROCEDURES,
|
||||
knowledgeSearch,
|
||||
knowledgeStatus,
|
||||
searchResultSchema,
|
||||
settingsDisableDocumentAi,
|
||||
settingsEnableDocumentAi,
|
||||
settingsGet,
|
||||
} from "./procedures.js"
|
||||
export type { DocumentStatus, EnablementReason, IndexFailure, KnowledgeBackend, ServiceState } from "./statuses.js"
|
||||
export {
|
||||
ENABLEMENT_REASONS,
|
||||
INDEX_FAILURES,
|
||||
KNOWLEDGE_BACKENDS,
|
||||
SERVICE_STATES,
|
||||
} from "./statuses.js"
|
||||
@@ -0,0 +1,187 @@
|
||||
// The procedure contracts of the knowledge module: schemas and names, no handler and no port.
|
||||
//
|
||||
// The module is a retrieval layer over the app's documents: a collection of text files is indexed
|
||||
// (in the worker: chunk, embed through the AI gateway, upsert into the vector store), searched
|
||||
// with cited snippets, and managed through an explicit, idempotent enablement flow. The enablement
|
||||
// procedure is named enableDocumentAi per the Document AI enablement contract; its permission is the module's
|
||||
// manage permission, so the admin gate is one line in the manifest.
|
||||
|
||||
import { procedure } from "@conusai-kit/core"
|
||||
import { z } from "zod"
|
||||
import { COLLECTION_CREATED, DOCUMENT_AI_DISABLED, DOCUMENT_AI_ENABLED } from "./constants.js"
|
||||
import type { DocumentStatus, ServiceState } from "./statuses.js"
|
||||
|
||||
const inProject = { org: z.string(), workspace: z.string(), project: z.string() }
|
||||
const inOrg = { org: z.string() }
|
||||
|
||||
export const collectionName = z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.regex(/^[a-z][a-z0-9-]*$/, "lower-case letters, digits and dashes")
|
||||
|
||||
export const collectionViewSchema = z.object({
|
||||
name: collectionName,
|
||||
documentCount: z.number().int(),
|
||||
indexedCount: z.number().int(),
|
||||
createdBy: z.string(),
|
||||
})
|
||||
export type CollectionView = z.output<typeof collectionViewSchema>
|
||||
|
||||
export const documentViewSchema = z.object({
|
||||
documentId: z.string(),
|
||||
collectionName: collectionName,
|
||||
fileId: z.string(),
|
||||
filename: z.string(),
|
||||
status: z.enum(["queued", "indexing", "indexed", "failed", "cancelled"]),
|
||||
error: z.string().nullable(),
|
||||
indexedAt: z.string().nullable(),
|
||||
requestedBy: z.string(),
|
||||
})
|
||||
export type DocumentView = z.output<typeof documentViewSchema>
|
||||
|
||||
export const searchResultSchema = z.object({
|
||||
fileId: z.string(),
|
||||
filename: z.string(),
|
||||
collectionName: collectionName,
|
||||
snippet: z.string(),
|
||||
/** 0..1; higher is closer. */
|
||||
score: z.number(),
|
||||
/** 1-based chunk position in the document: where a citation points. */
|
||||
chunk: z.number().int().min(1),
|
||||
})
|
||||
export type SearchResult = z.output<typeof searchResultSchema>
|
||||
|
||||
export const collectionList = procedure("knowledge.collection.list")
|
||||
.kind("read")
|
||||
.scope("project")
|
||||
.permission("knowledge.knowledge.search")
|
||||
.input(z.object({ ...inProject }))
|
||||
.output(z.object({ collections: z.array(collectionViewSchema) }))
|
||||
.contract()
|
||||
|
||||
export const collectionCreate = procedure("knowledge.collection.create")
|
||||
.kind("write")
|
||||
.scope("project")
|
||||
.permission("knowledge.collection.manage")
|
||||
.input(z.object({ ...inProject, name: collectionName }))
|
||||
.output(collectionViewSchema)
|
||||
.audit(COLLECTION_CREATED)
|
||||
.contract()
|
||||
|
||||
export const documentIndex = procedure("knowledge.document.index")
|
||||
.kind("write")
|
||||
.scope("project")
|
||||
.permission("knowledge.knowledge.manage")
|
||||
.input(z.object({ ...inProject, collectionName: collectionName, fileId: z.string() }))
|
||||
.output(documentViewSchema)
|
||||
.contract()
|
||||
|
||||
export const documentList = procedure("knowledge.document.list")
|
||||
.kind("read")
|
||||
.scope("project")
|
||||
.permission("knowledge.knowledge.search")
|
||||
.input(z.object({ ...inProject, collectionName: collectionName }))
|
||||
.output(z.object({ documents: z.array(documentViewSchema) }))
|
||||
.contract()
|
||||
|
||||
export const documentRemove = procedure("knowledge.document.remove")
|
||||
.kind("write")
|
||||
.scope("project")
|
||||
.permission("knowledge.knowledge.manage")
|
||||
.input(z.object({ ...inProject, collectionName: collectionName, fileId: z.string() }))
|
||||
.output(documentViewSchema)
|
||||
.contract()
|
||||
|
||||
/**
|
||||
* The retrieval call. It embeds the query through the AI gateway before it searches, so it is an
|
||||
* effect: a read procedure cannot leave the process. Exposed as an MCP tool on purpose (`.tool()`).
|
||||
*/
|
||||
export const knowledgeSearch = procedure("knowledge.search")
|
||||
.kind("effect")
|
||||
.scope("project")
|
||||
.permission("knowledge.knowledge.search")
|
||||
.tool()
|
||||
.input(
|
||||
z.object({
|
||||
...inProject,
|
||||
collectionName: collectionName,
|
||||
query: z.string().min(1).max(500),
|
||||
limit: z.number().int().min(1).max(25).optional(),
|
||||
}),
|
||||
)
|
||||
.output(z.object({ results: z.array(searchResultSchema) }))
|
||||
.contract()
|
||||
|
||||
export const knowledgeStatus = procedure("knowledge.status")
|
||||
.kind("read")
|
||||
.scope("project")
|
||||
.permission("knowledge.knowledge.search")
|
||||
.input(z.object({ ...inProject }))
|
||||
.output(
|
||||
z.object({
|
||||
/** The module flag's answer for this project. */
|
||||
enabled: z.boolean(),
|
||||
/** The service readiness, kept separate from the indexing indicator below. */
|
||||
service: z.object({
|
||||
state: z.enum(["registered", "indexing", "ready", "degraded", "disabled", "unavailable"]),
|
||||
reasons: z.array(z.string()),
|
||||
}),
|
||||
documents: z.object({
|
||||
queued: z.number().int(),
|
||||
indexing: z.number().int(),
|
||||
indexed: z.number().int(),
|
||||
failed: z.number().int(),
|
||||
cancelled: z.number().int(),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.contract()
|
||||
|
||||
export const settingsGet = procedure("knowledge.settings.get")
|
||||
.kind("read")
|
||||
.scope("org")
|
||||
.permission("knowledge.settings.manage")
|
||||
.input(z.object({ ...inOrg }))
|
||||
.output(
|
||||
z.object({
|
||||
enabled: z.boolean(),
|
||||
backend: z.enum(["vectors", "platform"]).nullable(),
|
||||
source: z.enum(["operator", "managed"]).nullable(),
|
||||
url: z.string().nullable(),
|
||||
}),
|
||||
)
|
||||
.contract()
|
||||
|
||||
export const settingsEnableDocumentAi = procedure("knowledge.settings.enableDocumentAi")
|
||||
.kind("write")
|
||||
.scope("org")
|
||||
.permission("knowledge.settings.manage")
|
||||
.input(z.object({ ...inOrg }))
|
||||
.output(z.object({ enabled: z.boolean(), backend: z.enum(["vectors", "platform"]) }))
|
||||
.audit(DOCUMENT_AI_ENABLED)
|
||||
.contract()
|
||||
|
||||
export const settingsDisableDocumentAi = procedure("knowledge.settings.disableDocumentAi")
|
||||
.kind("write")
|
||||
.scope("org")
|
||||
.permission("knowledge.settings.manage")
|
||||
.input(z.object({ ...inOrg }))
|
||||
.output(z.object({ enabled: z.boolean() }))
|
||||
.audit(DOCUMENT_AI_DISABLED)
|
||||
.contract()
|
||||
|
||||
export const KNOWLEDGE_PROCEDURES = [
|
||||
collectionList,
|
||||
collectionCreate,
|
||||
documentIndex,
|
||||
documentList,
|
||||
documentRemove,
|
||||
knowledgeSearch,
|
||||
knowledgeStatus,
|
||||
settingsGet,
|
||||
settingsEnableDocumentAi,
|
||||
settingsDisableDocumentAi,
|
||||
]
|
||||
|
||||
export type { DocumentStatus, ServiceState }
|
||||
@@ -0,0 +1,22 @@
|
||||
// The statuses a knowledge document moves through. `unavailable` and `disabled` are not rows: they
|
||||
// are the module's and the service's answers, and the UI maps all eight states from what a page
|
||||
// reads (the module flag, the service readiness, the document row).
|
||||
|
||||
export const DOCUMENT_STATUSES = ["queued", "indexing", "indexed", "failed", "cancelled"] as const
|
||||
export type DocumentStatus = (typeof DOCUMENT_STATUSES)[number]
|
||||
|
||||
/** The search backend a deployment binds: the local vector adapters, or the managed service. */
|
||||
export const KNOWLEDGE_BACKENDS = ["vectors", "platform"] as const
|
||||
export type KnowledgeBackend = (typeof KNOWLEDGE_BACKENDS)[number]
|
||||
|
||||
/** The readiness states the search port answers (the managed toolset's readiness + unavailable). */
|
||||
export const SERVICE_STATES = ["registered", "indexing", "ready", "degraded", "disabled", "unavailable"] as const
|
||||
export type ServiceState = (typeof SERVICE_STATES)[number]
|
||||
|
||||
/** The enablement flow's typed refusal reasons, surfaced in the UI as states. */
|
||||
export const ENABLEMENT_REASONS = ["plan_required", "feature_disabled", "quota_exhausted"] as const
|
||||
export type EnablementReason = (typeof ENABLEMENT_REASONS)[number]
|
||||
|
||||
/** The indexing failure codes a document row records. */
|
||||
export const INDEX_FAILURES = ["not_indexable", "too_large", "not_text", "vector_store_unavailable"] as const
|
||||
export type IndexFailure = (typeof INDEX_FAILURES)[number]
|
||||
Reference in New Issue
Block a user