@@ -0,0 +1,229 @@
|
||||
// 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: the platform's knowledge service reads, chunks and embeds it), 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 { AUTO_INDEX_CHANGED, COLLECTION_CREATED, DOCUMENT_AI_DISABLED, DOCUMENT_AI_ENABLED } from "./constants.js"
|
||||
import { type DocumentStatus, KNOWLEDGE_BACKENDS, KNOWLEDGE_REFUSALS, type 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(),
|
||||
page: z.number().int().min(1).optional(),
|
||||
blockId: z.string().optional(),
|
||||
chunkId: z.string().optional(),
|
||||
/** 0..1; higher is closer. */
|
||||
score: z.number(),
|
||||
/** Result ordinal only. Page/block/chunkId carry actual returned source locators when available. */
|
||||
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()
|
||||
|
||||
/**
|
||||
* The live collection's batch-get (contract C-05): the documents with these ids, in whichever collection of
|
||||
* the project, under the list's permission. An id of another tenant, of another project or of nothing is
|
||||
* left out of the answer.
|
||||
*/
|
||||
export const documentGetMany = procedure("knowledge.document.getMany")
|
||||
.kind("read")
|
||||
.scope("project")
|
||||
.permission("knowledge.knowledge.search")
|
||||
.input(z.object({ ...inProject, ids: z.array(z.string().min(1).max(64)).min(1).max(100) }))
|
||||
.output(z.object({ items: 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()),
|
||||
/** Why the service will not serve this deployment, when that is the answer: the page's own state. */
|
||||
refusal: z.enum(KNOWLEDGE_REFUSALS).optional(),
|
||||
}),
|
||||
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(KNOWLEDGE_BACKENDS).nullable(),
|
||||
}),
|
||||
)
|
||||
.contract()
|
||||
|
||||
/** Its checks and the managed link call the platform's APIs, so it is an effect, like the search. */
|
||||
export const settingsEnableDocumentAi = procedure("knowledge.settings.enableDocumentAi")
|
||||
.kind("effect")
|
||||
.scope("org")
|
||||
.permission("knowledge.settings.manage")
|
||||
.input(z.object({ ...inOrg }))
|
||||
.output(z.object({ enabled: z.boolean(), backend: z.enum(KNOWLEDGE_BACKENDS) }))
|
||||
.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()
|
||||
|
||||
/** Whether this project indexes its uploaded PDFs automatically, and into which collection. */
|
||||
export const autoIndexGet = procedure("knowledge.autoindex.get")
|
||||
.kind("read")
|
||||
.scope("project")
|
||||
.permission("knowledge.knowledge.search")
|
||||
.input(z.object({ ...inProject }))
|
||||
.output(z.object({ collectionName: collectionName.nullable() }))
|
||||
.contract()
|
||||
|
||||
/**
|
||||
* Turns automatic PDF indexing on (into an existing collection) or off (`null`). The administrator who
|
||||
* turns it on is the one every indexing job re-checks: their permission is the job's authority.
|
||||
*/
|
||||
export const autoIndexSet = procedure("knowledge.autoindex.set")
|
||||
.kind("write")
|
||||
.scope("project")
|
||||
.permission("knowledge.knowledge.manage")
|
||||
.input(z.object({ ...inProject, collectionName: collectionName.nullable() }))
|
||||
.output(z.object({ collectionName: collectionName.nullable() }))
|
||||
.audit(AUTO_INDEX_CHANGED)
|
||||
.contract()
|
||||
|
||||
export const KNOWLEDGE_PROCEDURES = [
|
||||
collectionList,
|
||||
collectionCreate,
|
||||
documentIndex,
|
||||
documentList,
|
||||
documentGetMany,
|
||||
documentRemove,
|
||||
knowledgeSearch,
|
||||
knowledgeStatus,
|
||||
settingsGet,
|
||||
settingsEnableDocumentAi,
|
||||
settingsDisableDocumentAi,
|
||||
autoIndexGet,
|
||||
autoIndexSet,
|
||||
]
|
||||
|
||||
export type { DocumentStatus, ServiceState }
|
||||
Reference in New Issue
Block a user