ConusAI vendor v0.1.0-beta.114
ci / gate (push) Waiting to run

This commit is contained in:
ConusAI vendor v0.1.0-beta.114
2026-11-14 09:56:45 +00:00
commit 1ce8d23c35
867 changed files with 122104 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
// The automatic intake's enabling-person metadata is an account-data share. The
// account kernel supplies the subject; this has no public procedure or bearer.
import type { TenantScope } from "@conusai-kit/core"
import { systemActor } from "@conusai-kit/core/worker"
import { DATA_SOURCE, SYSTEM_DATA } from "@conusai-kit/data"
import { type AccountDataContext, type AccountDataContribution, servedProjects } from "@conusai-kit/tenancy"
import { autoIndex } from "../contract/index.js"
async function* ownedOptIns(context: AccountDataContext) {
context.signal?.throwIfAborted()
const data = context.container.resolve(DATA_SOURCE)
const system = context.container.resolve(SYSTEM_DATA)
for await (const project of servedProjects(system)) {
context.signal?.throwIfAborted()
const scope: TenantScope = { level: "project", ...project, actor: systemActor("knowledge.account") }
for (let cursor: string | undefined; ; ) {
const page = await data
.forScope(scope)
.of(autoIndex)
.list({
where: [{ field: "enabledBy", op: "eq", value: context.subject }],
limit: 100,
...(cursor === undefined ? {} : { cursor }),
})
for (const row of page.items) yield { scope, row }
if (!page.hasMore || page.cursor === undefined) break
cursor = page.cursor
}
}
}
export const knowledgeAccountData: AccountDataContribution = {
key: "knowledge",
async export(context) {
const automaticIndexing = []
for await (const { scope, row } of ownedOptIns(context)) {
automaticIndexing.push({
orgId: scope.orgId,
workspaceId: scope.workspaceId,
projectId: scope.projectId,
collectionName: row.collectionName,
enabledAt: row.enabledAt,
})
}
return { automaticIndexing }
},
async erase(context) {
const data = context.container.resolve(DATA_SOURCE)
for await (const { scope, row } of ownedOptIns(context)) {
context.signal?.throwIfAborted()
await data.unitOfWork.run(scope, async (tx) => {
const repository = tx.of(autoIndex)
const current = repository.lock ? await repository.lock(row.id) : await repository.get(row.id)
// A different administrator may have replaced the opt-in since discovery.
if (!current || current.enabledBy !== context.subject) return
if (!repository.lock) await repository.update(current.id, {}, current.version)
await repository.delete(current.id)
})
}
},
}
+85
View File
@@ -0,0 +1,85 @@
// The enablement platform over the platform's own APIs, server-side. Eligibility reads the
// rollout flags (agent_document_tools, agent_knowledge_tools) from the flag snapshot and probes
// the AI tools registry for the document tool family — discovery is the cheap plan/binding/flag
// filter, and "a working OCR endpoint does not by itself make OCR a discoverable tool". Nothing is
// provisioned for the app: the platform's knowledge service owns the index (ADR-061). The quota
// pre-check has no platform API yet (the metering slice owns it); the indexing calls themselves
// carry the typed quota_exhausted refusal at run time.
import { createRegistryClient, ToolIndexingError } from "@conusai-sdk/ai"
import type { EnablementReason } from "../contract/index.js"
import type { DocumentAiPlatform } from "../core/ports.js"
export const AGENT_DOCUMENT_TOOLS_FLAG = "agent_document_tools"
export const AGENT_KNOWLEDGE_TOOLS_FLAG = "agent_knowledge_tools"
const FLAG_KEYS = [AGENT_DOCUMENT_TOOLS_FLAG, AGENT_KNOWLEDGE_TOOLS_FLAG] as const
export interface SdkDocumentAiPlatformOptions {
/** The deployment contract's API URL and token. */
apiUrl: string
token: string
fetch?: typeof fetch
}
export function sdkDocumentAiPlatform(options: SdkDocumentAiPlatformOptions): DocumentAiPlatform {
const http = options.fetch ?? fetch
const rolloutFlagsOff = async (): Promise<string | null> => {
try {
const response = await http(new URL(`${options.apiUrl.replace(/\/+$/, "")}/flags/snapshot`), {
headers: { authorization: `Bearer ${options.token}`, accept: "application/json" },
signal: AbortSignal.timeout(5_000),
})
if (!response.ok) return null // the flags service is not the eligibility authority
const body = (await response.json()) as { flags?: Array<{ key?: string; enabled?: boolean }> }
for (const flag of body.flags ?? []) {
if (
typeof flag.key === "string" &&
(FLAG_KEYS as readonly string[]).includes(flag.key) &&
flag.enabled === false
) {
return flag.key
}
}
return null
} catch {
return null
}
}
return {
async checkEligibility(): Promise<{ ok: true } | { ok: false; reason: EnablementReason; detail?: string }> {
// The rollout flags first: a kill switch or a disabled feature refuses before any probe.
const off = await rolloutFlagsOff()
if (off !== null) return { ok: false, reason: "feature_disabled", detail: `the ${off} flag is off` }
// The plan probe: can this deployment discover the document tool family at all?
try {
const registry = createRegistryClient({
env: { CONUSAI_API_URL: options.apiUrl, CONUSAI_API_TOKEN: options.token },
fetch: http,
})
const found = await registry.tools.search("read and index documents")
if (found.length === 0) {
return { ok: false, reason: "plan_required", detail: "the ai_tools plan capability lists no document tools" }
}
return { ok: true }
} catch (error) {
if (error instanceof ToolIndexingError) {
return { ok: false, reason: "feature_disabled", detail: "the tool registry is still indexing" }
}
return {
ok: false,
reason: "plan_required",
detail: "the AI tools registry did not answer the eligibility probe",
}
}
},
async indexingQuota() {
// No pre-check API exists on the platform yet; the call-time quota refusal is the hard
// gate. Until the limit endpoint lands, the admission check answers "within quota".
return Number.MAX_SAFE_INTEGER
},
}
}
+227
View File
@@ -0,0 +1,227 @@
// The knowledge jobs and the subscribers that enqueue them.
//
// A web request writes the document row and publishes the outbox event in one transaction; the
// module's own subscriber (running in the worker's outbox relay) turns the event into the job,
// with the (collection, file) pair as the singleton key — a redelivered event adds no second job.
//
// The relay delivers every record in its organization's scope, with a system actor, and the worker binds
// `KNOWLEDGE_SCOPE` to that delivery scope. The organization comes from there, never from the payload
// (decision AT-F6); only the workspace and the project come from the record, which this module or the files
// module wrote inside that organization's transaction, as `projectScopeOf` of @conusai-kit/files does. A job,
// unlike a delivery, runs in the project scope it was enqueued in.
import { type EventContext, internal, type JobDef, serviceRefusal, subscribe } from "@conusai-kit/core"
import { DATA_SOURCE, JOB_QUEUE } from "@conusai-kit/data"
import { fileDeleted, type ProcessorJobPayload, processorJobPayloadSchema } from "@conusai-kit/files"
import { NATIVE_JOB_ADMISSION } from "@conusai-kit/platform"
import { projects, workspaces } from "@conusai-kit/tenancy"
import { z } from "zod"
import {
AUTO_INDEX_JOB,
documentIndexRequested,
documentRemoveRequested,
INDEX_JOB,
REMOVE_JOB,
} from "../contract/index.js"
import { autoIndexUpload } from "../core/auto-index-job.js"
import { requestSourceRemoval } from "../core/documents.js"
import { indexDocument, removeDocument } from "../core/index-job.js"
import { KNOWLEDGE_SCOPE, type KnowledgeScope } from "../core/ports.js"
import { knowledgeDeps, knowledgeSearchDeps } from "./procedures.js"
export const documentPayload = z.object({
documentId: z.string(),
collectionName: z.string(),
fileId: z.string(),
/** A remove job's: the object to remove, resolved when the removal was asked for; absent only on an older record. */
storageKey: z.string().optional(),
})
export type DocumentJobPayload = z.output<typeof documentPayload>
export const indexJobKey = (collectionName: string, fileId: string): string =>
`knowledge.index.${collectionName}.${fileId}`
export const removeJobKey = (collectionName: string, fileId: string): string =>
`knowledge.remove.${collectionName}.${fileId}`
/**
* The project a delivery acts in: the delivery's organization and actor, and the record's workspace and project.
* A record that names another organization is refused here rather than acted on there, and so is one whose workspace
* the organization does not have or whose project is not in that workspace: both are read in the delivery's own
* organization before anything is enqueued or changed. That refusal can never succeed on a retry; the relay has no
* terminal-failure path yet, so it is retried until marked stuck, with the reason (a kit-jobs gap, recorded in the
* template lessons). An archived project is acted on: an archive hides a project and does not end its work
* (kit-tenancy `suspension.ts`).
*/
async function projectScopeOf(
ctx: EventContext,
payload: { orgId: string; workspaceId: string; projectId: string },
): Promise<KnowledgeScope & { level: "project" }> {
const ambient = ctx.container.resolve(KNOWLEDGE_SCOPE)
if (ambient.orgId !== payload.orgId) {
throw internal(`a knowledge event of organization ${payload.orgId} was delivered in ${ambient.orgId}`)
}
const data = ctx.container.resolve(DATA_SOURCE)
const inOrg = { level: "org" as const, actor: ambient.actor, orgId: ambient.orgId }
if (!(await data.forScope(inOrg).of(workspaces).get(payload.workspaceId))) {
throw internal(
`a knowledge event names workspace ${payload.workspaceId}, which organization ${ambient.orgId} does not have`,
)
}
const inWorkspace = {
level: "workspace" as const,
actor: ambient.actor,
orgId: ambient.orgId,
workspaceId: payload.workspaceId,
}
if (!(await data.forScope(inWorkspace).of(projects).get(payload.projectId))) {
throw internal(
`a knowledge event names project ${payload.projectId}, which is not in workspace ${payload.workspaceId}`,
)
}
return {
level: "project",
actor: ambient.actor,
orgId: ambient.orgId,
workspaceId: payload.workspaceId,
projectId: payload.projectId,
}
}
export const indexJob: JobDef<DocumentJobPayload> = {
payload: documentPayload,
async handler(payload, ctx) {
const scope = ctx.container.resolve(KNOWLEDGE_SCOPE)
if (scope.level !== "project") throw internal(`a knowledge job runs in a project scope, not a ${scope.level} one`)
await indexDocument(
{
...knowledgeSearchDeps(ctx.container),
beforeIndex: async (subject) => {
if (!ctx.signal || !ctx.container.has(NATIVE_JOB_ADMISSION)) throw serviceRefusal("observation_missing")
ctx.signal.throwIfAborted()
await ctx.container.resolve(NATIVE_JOB_ADMISSION).authorize(scope, INDEX_JOB, subject, ctx.signal)
},
},
scope,
payload.documentId,
{
...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
...(ctx.finalAttempt === undefined ? {} : { finalAttempt: ctx.finalAttempt }),
},
)
},
}
/**
* The automatic PDF intake (core-11): the files module enqueues it for every stored upload this module's
* processor wants, in the upload's project. It acts for the administrator who opted the project in, so
* the worker's native admission checks that administrator's permission and the platform's services again
* before each step that leaves the process, as the index job does for the person who asked.
*/
export const autoIndexJob: JobDef<ProcessorJobPayload> = {
payload: processorJobPayloadSchema,
async handler(payload, ctx) {
const scope = ctx.container.resolve(KNOWLEDGE_SCOPE)
if (scope.level !== "project") throw internal(`a knowledge job runs in a project scope, not a ${scope.level} one`)
await autoIndexUpload(
{
...knowledgeSearchDeps(ctx.container),
beforeIndex: async (subject) => {
if (!ctx.signal || !ctx.container.has(NATIVE_JOB_ADMISSION)) throw serviceRefusal("observation_missing")
ctx.signal.throwIfAborted()
await ctx.container.resolve(NATIVE_JOB_ADMISSION).authorize(scope, AUTO_INDEX_JOB, subject, ctx.signal)
},
},
scope,
payload,
{
...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
...(ctx.finalAttempt === undefined ? {} : { finalAttempt: ctx.finalAttempt }),
},
)
},
}
export const removeJob: JobDef<DocumentJobPayload> = {
payload: documentPayload,
async handler(payload, ctx) {
const scope = ctx.container.resolve(KNOWLEDGE_SCOPE)
if (scope.level !== "project") throw internal(`a knowledge job runs in a project scope, not a ${scope.level} one`)
await removeDocument(knowledgeSearchDeps(ctx.container), scope, payload.documentId, payload.storageKey)
},
}
async function onDocumentIndexRequested(
event: {
name: string
payload: {
orgId: string
workspaceId: string
projectId: string
documentId: string
collectionName: string
fileId: string
}
},
ctx: EventContext,
): Promise<void> {
const scope = await projectScopeOf(ctx, event.payload)
await ctx.container.resolve(JOB_QUEUE).enqueue(
scope,
INDEX_JOB,
{
documentId: event.payload.documentId,
collectionName: event.payload.collectionName,
fileId: event.payload.fileId,
},
{
key: indexJobKey(event.payload.collectionName, event.payload.fileId),
},
)
}
async function onDocumentRemoveRequested(
event: {
name: string
payload: {
orgId: string
workspaceId: string
projectId: string
documentId: string
collectionName: string
fileId: string
storageKey?: string | undefined
}
},
ctx: EventContext,
): Promise<void> {
const scope = await projectScopeOf(ctx, event.payload)
await ctx.container.resolve(JOB_QUEUE).enqueue(
scope,
REMOVE_JOB,
{
documentId: event.payload.documentId,
collectionName: event.payload.collectionName,
fileId: event.payload.fileId,
...(event.payload.storageKey === undefined ? {} : { storageKey: event.payload.storageKey }),
},
{
key: removeJobKey(event.payload.collectionName, event.payload.fileId),
},
)
}
async function onSourceDeleted(
event: { payload: { orgId: string; workspaceId: string; projectId: string; fileId: string; storageKey: string } },
ctx: EventContext,
): Promise<void> {
const scope = await projectScopeOf(ctx, event.payload)
// The file's row is gone by now: the key it had travels with the removal (F407).
await requestSourceRemoval(knowledgeDeps(ctx.container), scope, event.payload.fileId, event.payload.storageKey)
}
export const KNOWLEDGE_JOBS = { [INDEX_JOB]: indexJob, [REMOVE_JOB]: removeJob, [AUTO_INDEX_JOB]: autoIndexJob }
export const KNOWLEDGE_SUBSCRIPTIONS = [
subscribe(fileDeleted, onSourceDeleted),
subscribe(documentIndexRequested, onDocumentIndexRequested),
subscribe(documentRemoveRequested, onDocumentRemoveRequested),
]
@@ -0,0 +1,280 @@
// The managed search service: the platform's documents-index service through the kit-platform
// Knowledge client. Indexing and search run server-side there (the service owns chunking,
// embeddings and the vector store); this module supplies the job lifecycle and the citations it
// answers with.
//
// Contract notes (the service's real shapes): the index submission carries the content's SHA-256
// (computed here from the job's bytes) as its version assertion; a quota denial surfaces on the
// job, which the poll below reads as the job's typed error. Blob pathnames and returned citations
// resolve through the files module's scoped rows, so an opaque file id never becomes a pathname
// and a foreign or stale service reference never becomes a valid app hit.
import { createHash } from "node:crypto"
import { setTimeout as delay } from "node:timers/promises"
import {
type FilesDeps,
findFileStorageByKey,
getFileStorage,
platformBlobPathname,
platformBlobStorageKey,
} from "@conusai-kit/files"
import { KNOWLEDGE_LIMITS, type Knowledge, type KnowledgeClient, KnowledgeError } from "@conusai-kit/platform"
import type { KnowledgeHit, KnowledgePassageIndex, KnowledgeScope, KnowledgeSearchService } from "../core/ports.js"
import { platformKnowledgeReadiness } from "./readiness.js"
import { permanentRefusal } from "./refusals.js"
/** The service's tenant partition of a project: a search of the project and its indexed documents agree on it. */
const tenantOf = (scope: KnowledgeScope): string =>
createHash("sha256")
.update(JSON.stringify([scope.orgId, scope.workspaceId, scope.projectId]))
.digest("hex")
/** A passage's reference: the file's own plus `#p{page}.{index}` (the automatic PDF intake, core-11). */
const passageRef = (documentRef: string, passage: { page: number; index: number }): string =>
`${documentRef}#p${passage.page}.${passage.index}`
const PASSAGE_REF = /^(.+)#p([1-9][0-9]*)\.(0|[1-9][0-9]*)$/
/**
* `environment` is the deployment's (`CONUSAI_ENVIRONMENT`): the service keeps a collection in one environment,
* and a deployment token reaches only its own.
*/
export function platformKnowledgeSearch(
client: Knowledge,
files: FilesDeps,
options: { environment?: string | undefined; timeoutMs?: number; intervalMs?: number } = {},
): KnowledgeSearchService {
const environment = options.environment?.trim()
const configuredEnvironment = (): string => {
if (!environment)
throw new KnowledgeError("configuration_missing", "CONUSAI_ENVIRONMENT is required for knowledge indexing")
return environment
}
const unwrap = (error: unknown): never => {
// The client's errors are already typed by reason; a non-knowledge failure is unavailable.
if (error instanceof KnowledgeError) throw error
throw new KnowledgeError(
"runtime_unavailable",
error instanceof Error ? error.message : "the knowledge service failed",
true,
)
}
/** The same, for an index job: a refusal waiting cannot fix is recorded on the document, not retried. */
const unwrapJob = (error: unknown): never => {
if (error instanceof KnowledgeError) {
const permanent = permanentRefusal(error)
if (permanent !== undefined) throw Object.assign(error, { permanent })
}
return unwrap(error)
}
const documentRef = async (scope: KnowledgeScope, fileId: string): Promise<string> => {
const reference = await getFileStorage(files, scope, fileId)
return platformBlobPathname(scope, reference.storageKey)
}
return {
readiness(scope) {
if (!environment)
return Promise.resolve({
state: "unavailable",
reasons: ["CONUSAI_ENVIRONMENT is not set"],
refusal: "configuration_missing",
})
return platformKnowledgeReadiness(client).readiness(scope)
},
async index(scope, input) {
try {
// The service answers 404 for a collection it does not hold. An app's collection is only a row of
// this module (a write cannot leave the process), so the first index creates it; creating it again
// finds it.
const activeEnvironment = configuredEnvironment()
const reference = await documentRef(scope, input.fileId)
const signal = AbortSignal.any([
AbortSignal.timeout(options.timeoutMs ?? 300_000),
...(input.signal ? [input.signal] : []),
])
signal.throwIfAborted()
await client.createCollection({ collection: input.collectionName, environment: activeEnvironment }, { signal })
const { jobId } = await client.index(
{
collection: input.collectionName,
documentRef: reference,
options: {
visibility: "tenant",
tenant: tenantOf(scope),
contentSha256: createHash("sha256").update(input.bytes).digest("hex"),
},
},
{ signal },
)
// The service's job is durable; poll until it leaves the queue. A caller-side cancel is
// the job's own AbortSignal, checked between polls.
for (;;) {
signal.throwIfAborted()
const job = await client.job(jobId, { signal })
if (job.status === "succeeded") return
if (job.status === "failed")
throw new KnowledgeError(
job.error === "quota-exhausted" ? "quota_exhausted" : "runtime_unavailable",
job.error ?? "the indexing job failed",
)
if (job.status === "cancelled")
throw new KnowledgeError("runtime_unavailable", "the indexing job was cancelled")
await delay(options.intervalMs ?? 1_000, undefined, { signal })
}
} catch (error) {
return unwrapJob(error)
}
},
async search(scope, input) {
try {
configuredEnvironment()
const results = await client.search({
collection: input.collectionName,
query: input.query,
limit: input.limit,
tenant: tenantOf(scope),
})
const hits: KnowledgeHit[] = []
for (const [at, hit] of results.entries()) {
// A passage of a PDF the intake indexed names its file's reference, and cites from its metadata:
// the service reports pages only for sources it parsed itself.
const passage = PASSAGE_REF.exec(hit.documentRef)
const storageKey = platformBlobStorageKey(scope, passage?.[1] ?? hit.documentRef)
if (storageKey === null) continue
const reference = await findFileStorageByKey(files, scope, storageKey)
if (reference === null) continue
const metadata = "metadata" in hit ? (hit.metadata as Readonly<Record<string, string>>) : {}
if (passage && metadata.file_id !== reference.file.fileId) continue
const page = passage ? Number(passage[2]) : hit.citations[0]?.page
const blockId = passage ? metadata.block_ids?.split(",")[0] || undefined : hit.citations[0]?.blockId
hits.push({
fileId: reference.file.fileId,
filename: reference.file.filename,
collectionName: input.collectionName,
score: hit.score,
chunk: at + 1,
snippet: hit.snippet,
...(page === undefined ? {} : { page }),
...(blockId === undefined ? {} : { blockId }),
...("chunkId" in hit && typeof hit.chunkId === "string" ? { chunkId: hit.chunkId } : {}),
})
}
return hits
} catch (error) {
// Nothing has been indexed into this collection yet, so the service has not heard of it.
if (error instanceof KnowledgeError && error.reason === "unavailable_resource") return []
return unwrap(error)
}
},
async remove(scope, input) {
try {
// Every removal asked for since F407 names its object; only a record from before it is looked up.
const reference =
input.storageKey === undefined
? await documentRef(scope, input.fileId)
: platformBlobPathname(scope, input.storageKey)
await client.remove(reference)
} catch (error) {
return unwrap(error)
}
},
}
}
/**
* The automatic PDF intake's index (core-11): one inline item per passage, in the project's tenant partition,
* under the file's Blob reference plus `#p{page}.{index}`, with its page, its blocks and its file in metadata.
* Items go at most the service's count per call, and every item's durable job is waited for; a refusal waiting
* cannot fix is the document's, as with a whole file.
*/
export function platformPassageIndex(
client: Pick<KnowledgeClient, "createCollection" | "indexInline" | "job" | "remove">,
files: FilesDeps,
options: { environment?: string | undefined; timeoutMs?: number; intervalMs?: number } = {},
): KnowledgePassageIndex {
const environment = options.environment?.trim()
const failure = (error: unknown): never => {
if (error instanceof KnowledgeError) {
const permanent = permanentRefusal(error)
if (permanent !== undefined) throw Object.assign(error, { permanent })
throw error
}
throw new KnowledgeError(
"runtime_unavailable",
error instanceof Error ? error.message : "the knowledge service failed",
true,
)
}
return {
async reference(scope, fileId) {
const stored = await getFileStorage(files, scope, fileId)
return platformBlobPathname(scope, stored.storageKey)
},
async index(scope, input) {
try {
if (!environment)
throw new KnowledgeError("configuration_missing", "CONUSAI_ENVIRONMENT is required for knowledge indexing")
const signal = AbortSignal.any([
AbortSignal.timeout(options.timeoutMs ?? 300_000),
...(input.signal ? [input.signal] : []),
])
await client.createCollection({ collection: input.collectionName, environment }, { signal })
const tenant = tenantOf(scope)
const jobs: string[] = []
for (let at = 0; at < input.passages.length; at += KNOWLEDGE_LIMITS.inlineItems) {
signal.throwIfAborted()
const accepted = await client.indexInline({
collection: input.collectionName,
items: input.passages.slice(at, at + KNOWLEDGE_LIMITS.inlineItems).map((passage) => ({
documentRef: passageRef(input.documentRef, passage),
text: passage.text,
visibility: "tenant" as const,
tenant,
metadata: {
page: String(passage.page),
...(passage.blockIds.length === 0 ? {} : { block_ids: passage.blockIds.join(",") }),
file_id: input.fileId,
},
})),
})
jobs.push(...accepted.map((job) => job.jobId))
}
for (const jobId of jobs) {
for (;;) {
signal.throwIfAborted()
const job = await client.job(jobId, { signal })
if (job.status === "succeeded") break
if (job.status === "failed")
throw new KnowledgeError(
job.error === "quota-exhausted" ? "quota_exhausted" : "runtime_unavailable",
job.error ?? "the indexing job failed",
)
if (job.status === "cancelled")
throw new KnowledgeError("runtime_unavailable", "the indexing job was cancelled")
await delay(options.intervalMs ?? 1_000, undefined, { signal })
}
}
} catch (error) {
return failure(error)
}
},
async remove(_scope, input) {
for (const passage of input.passages) {
try {
await client.remove(passageRef(input.documentRef, passage))
} catch (error) {
// A passage the service never indexed (the run was refused before it) is already removed.
if (error instanceof KnowledgeError && error.reason === "unavailable_resource") continue
failure(error)
}
}
},
}
}
+154
View File
@@ -0,0 +1,154 @@
// The handlers of the knowledge module's procedures: each resolves its ports from the request's
// container and calls one use case. No rule lives here.
import type { Container } from "@conusai-kit/core"
import { implement } from "@conusai-kit/core"
import { DATA_SOURCE } from "@conusai-kit/data"
import { filesDeps } from "@conusai-kit/files"
import {
autoIndexGet,
autoIndexSet,
collectionCreate,
collectionList,
documentGetMany,
documentIndex,
documentList,
documentRemove,
knowledgeSearch,
knowledgeStatus,
settingsDisableDocumentAi,
settingsEnableDocumentAi,
settingsGet,
} from "../contract/index.js"
import { getAutoIndex, setAutoIndex } from "../core/auto-index.js"
import { createCollection, listCollections } from "../core/collections.js"
import { getManyDocuments, listDocuments, requestIndex, requestRemove } from "../core/documents.js"
import {
flagsOf,
KNOWLEDGE_ENABLEMENT,
KNOWLEDGE_PASSAGES,
KNOWLEDGE_READER,
KNOWLEDGE_READINESS,
KNOWLEDGE_SEARCH,
type KnowledgeDeps,
} from "../core/ports.js"
import { searchKnowledge } from "../core/search.js"
import { disableDocumentAi, enableDocumentAi, getSettings, type SettingsDeps } from "../core/settings.js"
import { type KnowledgeStatusDeps, knowledgeStatusOf } from "../core/status.js"
import { knowledgeRefusal } from "./refusals.js"
/** The ports a write or read handler needs. Never the search service: that port is an effect, and
* resolving it from either is a kind violation. */
export function knowledgeDeps(container: Container): KnowledgeDeps {
const data = container.resolve(DATA_SOURCE)
return { data, files: filesDeps(container), flags: flagsOf(container, data) }
}
/**
* The effect handler's deps: the search service, bound in the processes that may leave the process, and the
* PDF intake's reader, passage index and enablement checks where the worker binds the reader (the jobs; a
* request never reads a PDF).
*/
export function knowledgeSearchDeps(container: Container): KnowledgeDeps {
return {
...knowledgeDeps(container),
search: container.resolve(KNOWLEDGE_SEARCH),
...(container.has(KNOWLEDGE_READER) ? { reader: container.resolve(KNOWLEDGE_READER) } : {}),
...(container.has(KNOWLEDGE_PASSAGES) ? { passages: container.resolve(KNOWLEDGE_PASSAGES) } : {}),
...(container.has(KNOWLEDGE_READER) ? { enablement: container.resolve(KNOWLEDGE_ENABLEMENT) } : {}),
}
}
/** The status read's deps: the backend's readiness, which a read may resolve, instead of the search service. */
function knowledgeStatusDeps(container: Container): KnowledgeStatusDeps {
const data = container.resolve(DATA_SOURCE)
return {
data,
flags: flagsOf(container, data),
...(container.has(KNOWLEDGE_READINESS) ? { readiness: container.resolve(KNOWLEDGE_READINESS) } : {}),
}
}
/** The enablement flow's deps, for the effect that leaves the process. The other settings procedures read and write the module's own row. */
function settingsDeps(container: Container): SettingsDeps {
return { data: container.resolve(DATA_SOURCE), platform: container.resolve(KNOWLEDGE_ENABLEMENT) }
}
const settingsDataOf = (container: Container) => ({ data: container.resolve(DATA_SOURCE) })
export const collectionListImpl = implement(collectionList, ({ ctx }) =>
listCollections(knowledgeDeps(ctx.container), ctx.scope),
)
export const collectionCreateImpl = implement(collectionCreate, ({ input, ctx }) =>
createCollection(knowledgeDeps(ctx.container), ctx.scope, input.name),
)
export const documentIndexImpl = implement(documentIndex, ({ input, ctx }) =>
requestIndex(knowledgeDeps(ctx.container), ctx.scope, input),
)
export const documentListImpl = implement(documentList, ({ input, ctx }) =>
listDocuments(knowledgeDeps(ctx.container), ctx.scope, input.collectionName),
)
export const documentGetManyImpl = implement(documentGetMany, async ({ input, ctx }) => ({
items: await getManyDocuments(knowledgeDeps(ctx.container), ctx.scope, input.ids),
}))
export const documentRemoveImpl = implement(documentRemove, ({ input, ctx }) =>
requestRemove(knowledgeDeps(ctx.container), ctx.scope, input),
)
export const knowledgeSearchImpl = implement(knowledgeSearch, async ({ input, ctx }) => {
try {
return await searchKnowledge(knowledgeSearchDeps(ctx.container), ctx.scope, {
collectionName: input.collectionName,
query: input.query,
...(input.limit === undefined ? {} : { limit: input.limit }),
})
} catch (error) {
throw knowledgeRefusal(error)
}
})
export const knowledgeStatusImpl = implement(knowledgeStatus, ({ ctx }) =>
knowledgeStatusOf(knowledgeStatusDeps(ctx.container), ctx.scope),
)
export const settingsGetImpl = implement(settingsGet, ({ ctx }) =>
getSettings(settingsDataOf(ctx.container), ctx.scope),
)
export const settingsEnableDocumentAiImpl = implement(settingsEnableDocumentAi, async ({ ctx }) => {
const settings = await enableDocumentAi(settingsDeps(ctx.container), ctx.scope)
return { enabled: settings.enabled, backend: settings.backend ?? "platform" }
})
export const settingsDisableDocumentAiImpl = implement(settingsDisableDocumentAi, ({ ctx }) =>
disableDocumentAi(settingsDataOf(ctx.container), ctx.scope),
)
export const autoIndexGetImpl = implement(autoIndexGet, ({ ctx }) =>
getAutoIndex(settingsDataOf(ctx.container), ctx.scope),
)
export const autoIndexSetImpl = implement(autoIndexSet, ({ input, ctx }) =>
setAutoIndex(knowledgeDeps(ctx.container), ctx.scope, input.collectionName),
)
export const KNOWLEDGE_PROCEDURE_IMPLS = [
collectionListImpl,
collectionCreateImpl,
documentIndexImpl,
documentListImpl,
documentGetManyImpl,
documentRemoveImpl,
knowledgeSearchImpl,
knowledgeStatusImpl,
settingsGetImpl,
settingsEnableDocumentAiImpl,
settingsDisableDocumentAiImpl,
autoIndexGetImpl,
autoIndexSetImpl,
]
+46
View File
@@ -0,0 +1,46 @@
// The automatic PDF intake's reader (core-11): the platform's document reader (`DocumentReader`, kit-platform's
// `@conusai-sdk/ocr` wired to the deployment contract) and the read document's passages
// (`@conusai-sdk/ocr/passages`), cut to the knowledge service's limit for one indexed item. The one thing this
// adapter decides is what is permanent: a job the platform failed, or a document whose blocks no metadata
// value can cite (`passages` throws `RangeError`), is not indexable; anything else is thrown for the queue.
import type { DocumentReader } from "@conusai-kit/platform"
import { KNOWLEDGE_LIMITS } from "@conusai-kit/platform"
import { OcrError } from "@conusai-sdk/ocr"
import { passages } from "@conusai-sdk/ocr/passages"
import type { KnowledgeReader } from "../core/ports.js"
export interface SdkKnowledgeReaderOptions {
/** Poll interval of `wait`, in milliseconds. */
readonly intervalMs?: number
/** Give up on one job after this long; the queue's retry submits again under the same key. */
readonly timeoutMs?: number
}
export function sdkKnowledgeReader(
reader: Pick<DocumentReader, "submit" | "wait">,
options: SdkKnowledgeReaderOptions = {},
): KnowledgeReader {
return {
async read(bytes, { filename, idempotencyKey, signal }) {
try {
const job = await reader.submit(bytes, { filename, idempotencyKey })
const document = await reader.wait(job.id, {
...(options.intervalMs === undefined ? {} : { intervalMs: options.intervalMs }),
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
...(signal ? { signal } : {}),
})
return passages(document, { maxBytes: KNOWLEDGE_LIMITS.inlineTextBytes }).map((passage) => ({
page: passage.page,
index: passage.index,
blockIds: passage.blockIds,
text: passage.text,
}))
} catch (error) {
if ((error instanceof OcrError && error.type === "job-failed") || error instanceof RangeError)
throw Object.assign(error, { permanent: "not_indexable" })
throw error
}
},
}
}
+25
View File
@@ -0,0 +1,25 @@
// The platform service's readiness, on its own so the status read can ask it without the search service
// (`KNOWLEDGE_READINESS`): the search adapter answers the same way, from here.
import { type Knowledge, KnowledgeError } from "@conusai-kit/platform"
import type { KnowledgeReadiness } from "../core/ports.js"
import { refusalOfReason, stateOfRefusal } from "./refusals.js"
/**
* The platform's managed service answers for itself. A refusal is the service's answer, as a typed code
* the page shows in the reader's language; a failure to ask it is its being unavailable.
*/
export const platformKnowledgeReadiness = (client: Pick<Knowledge, "readiness">): KnowledgeReadiness => ({
async readiness() {
try {
const answer = await client.readiness()
return { state: answer.state, reasons: [...answer.reasons] }
} catch (error) {
if (error instanceof KnowledgeError) {
const refusal = refusalOfReason(error.reason)
return { state: stateOfRefusal(refusal), reasons: [], ...(refusal === undefined ? {} : { refusal }) }
}
return { state: "unavailable", reasons: [], refusal: "runtime_unavailable" }
}
},
})
+77
View File
@@ -0,0 +1,77 @@
// A refusal of the platform's knowledge service as this module's one closed set of codes
// (`KNOWLEDGE_REFUSALS`), which the pages and forms show in the reader's language. A refusal that is the
// deployment's or the platform's says so; the service's own words stay in `detail`, for the log and the API.
import { AppError, notFound } from "@conusai-kit/core"
import { KnowledgeError, type KnowledgeReasonCode } from "@conusai-kit/platform"
import type { KnowledgeRefusal, ServiceState } from "../contract/index.js"
const REFUSAL: Readonly<Record<Exclude<KnowledgeReasonCode, "unavailable_resource">, KnowledgeRefusal>> = {
plan_required: "plan_required",
feature_disabled: "feature_disabled",
quota_exhausted: "quota_exhausted",
budget_exhausted: "budget_exhausted",
// The service refused the token itself: `knowledge.collections:use` is missing, `*` never carries it.
forbidden: "scope_missing",
indexing: "indexing",
approval_required: "approval_required",
runtime_unavailable: "runtime_unavailable",
configuration_missing: "configuration_missing",
// The platform refused the token's identity: nothing the person can act on, the app is not served.
unauthenticated: "runtime_unavailable",
}
const STATUS: Readonly<Record<KnowledgeRefusal, number>> = {
plan_required: 403,
feature_disabled: 403,
approval_required: 403,
quota_exhausted: 429,
budget_exhausted: 429,
scope_missing: 503,
indexing: 503,
runtime_unavailable: 503,
configuration_missing: 503,
}
const TITLE: Readonly<Record<KnowledgeRefusal, string>> = {
plan_required: "The organization's plan does not include document search",
feature_disabled: "Document search is disabled by policy",
approval_required: "The platform asks for an approval first",
quota_exhausted: "The indexing quota is exhausted",
budget_exhausted: "The AI budget is exhausted",
scope_missing: "This deployment's token does not hold knowledge.collections:use",
indexing: "The knowledge service is building its index",
runtime_unavailable: "The knowledge service is unavailable",
configuration_missing: "The knowledge deployment configuration is incomplete",
}
/** The typed refusal of a service error; a resource it does not hold has none, it is simply not there. */
export function refusalOfReason(reason: KnowledgeReasonCode): KnowledgeRefusal | undefined {
return reason === "unavailable_resource" ? undefined : REFUSAL[reason]
}
/** The readiness state a refusal puts the service in: a plan or a policy is "disabled", not "down". */
export function stateOfRefusal(refusal: KnowledgeRefusal | undefined): ServiceState {
if (refusal === "plan_required" || refusal === "feature_disabled") return "disabled"
return refusal === "indexing" ? "indexing" : "unavailable"
}
/** The app error for anything the service threw; a programming error passes through as it is. */
export function knowledgeRefusal(error: unknown): unknown {
if (!(error instanceof KnowledgeError)) return error
const refusal = refusalOfReason(error.reason)
if (refusal === undefined) return notFound(error.message)
return new AppError({
type: `/problems/${refusal}`,
title: TITLE[refusal],
status: STATUS[refusal],
code: refusal,
detail: error.message,
})
}
/** A refusal waiting cannot fix: the index job records it on the document instead of retrying it. */
export function permanentRefusal(error: KnowledgeError): KnowledgeRefusal | undefined {
const refusal = refusalOfReason(error.reason)
return refusal === "indexing" || refusal === "runtime_unavailable" ? undefined : refusal
}