@@ -0,0 +1,180 @@
|
||||
// The ports the knowledge module's use cases are written against, and the tokens the composition
|
||||
// roots bind. Core imports no SDK and no adapter (spec §2): the search service and the enablement
|
||||
// platform are shapes, and adapters/ is the only place an SDK exists.
|
||||
|
||||
import { type Container, type TenantScope, token } from "@conusai-kit/core"
|
||||
import type { DataSource } from "@conusai-kit/data"
|
||||
import type { FilesDeps } from "@conusai-kit/files"
|
||||
import type { EnablementReason, KnowledgeRefusal, ServiceState } from "../contract/index.js"
|
||||
import { config, KNOWLEDGE_FLAG_DEFAULTS } from "../contract/index.js"
|
||||
|
||||
/** A project scope: collections and documents live at this level. */
|
||||
export type KnowledgeScope = TenantScope & { workspaceId: string; projectId: string }
|
||||
|
||||
/** An org scope: the enablement configuration is the organization's own. */
|
||||
export type KnowledgeOrgScope = TenantScope & { orgId: string }
|
||||
|
||||
export interface KnowledgeDeps {
|
||||
readonly data: DataSource
|
||||
readonly files: FilesDeps
|
||||
readonly flags: KnowledgeFlags
|
||||
readonly beforeIndex?: (subjectUserId: string) => Promise<void>
|
||||
/**
|
||||
* The search service. Present for the effect procedure (search); a read or write procedure's deps
|
||||
* never carry it, because resolving an effect port from either is a kind violation — the write
|
||||
* only records the row and publishes the event, and the status read asks `KNOWLEDGE_READINESS`.
|
||||
*/
|
||||
readonly search?: KnowledgeSearchService | undefined
|
||||
/** The document reader and the passage index, for the automatic PDF intake (core-11); bound in the worker. */
|
||||
readonly reader?: KnowledgeReader | undefined
|
||||
readonly passages?: KnowledgePassageIndex | undefined
|
||||
/** The organization's eligibility and indexing quota, which the PDF intake checks before each read. */
|
||||
readonly enablement?: DocumentAiPlatform | undefined
|
||||
}
|
||||
|
||||
/** One cited search answer. */
|
||||
export interface KnowledgeHit {
|
||||
fileId: string
|
||||
filename: string
|
||||
collectionName: string
|
||||
/** 0..1; higher is closer. */
|
||||
score: number
|
||||
chunk: number
|
||||
snippet: string
|
||||
page?: number
|
||||
blockId?: string
|
||||
chunkId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The search service: index a document, search a collection with citations, remove a document, and
|
||||
* report readiness. Its one adapter is the platform's managed documents-index service (ADR-061), which
|
||||
* owns chunking, embeddings and the vector store. Every call leaves the process, so this port is an
|
||||
* effect. `refusal` is why the service will not serve this deployment, when that is the answer.
|
||||
*/
|
||||
export interface KnowledgeSearchService {
|
||||
readiness(scope: KnowledgeScope): Promise<{ state: ServiceState; reasons: string[]; refusal?: KnowledgeRefusal }>
|
||||
index(
|
||||
scope: KnowledgeScope,
|
||||
input: { collectionName: string; fileId: string; filename: string; bytes: Uint8Array; signal?: AbortSignal },
|
||||
): Promise<void>
|
||||
search(
|
||||
scope: KnowledgeScope,
|
||||
input: { collectionName: string; query: string; limit: number; actorId: string },
|
||||
): Promise<readonly KnowledgeHit[]>
|
||||
/** `storageKey` names the object, resolved when the removal was asked for; only an older record lacks it, and then the file is looked up. */
|
||||
remove(scope: KnowledgeScope, input: { collectionName: string; fileId: string; storageKey?: string }): Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* One passage of a read PDF, as the automatic intake indexes it: its page (1-based), its place on that
|
||||
* page (0-based; with the page, its stable name across reads of the same document), the document's own
|
||||
* block ids it holds lines of, and its text.
|
||||
*/
|
||||
export interface KnowledgePassage {
|
||||
readonly page: number
|
||||
readonly index: number
|
||||
readonly blockIds: readonly string[]
|
||||
readonly text: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The platform's document reader, as the automatic intake uses it: a PDF in, its passages out. The
|
||||
* same `idempotencyKey` reads the same document once. A document it can never make passages of throws
|
||||
* with `permanent: "not_indexable"`; anything else is transient, and the queue retries the job.
|
||||
*/
|
||||
export interface KnowledgeReader {
|
||||
read(
|
||||
bytes: Uint8Array,
|
||||
options: { filename: string; idempotencyKey: string; signal?: AbortSignal },
|
||||
): Promise<readonly KnowledgePassage[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* The knowledge service's inline index, one item per passage, each under the file's reference plus its
|
||||
* page and place. The reference is asked for first, so the caller can record what it is about to index
|
||||
* before any of it is sent, and `remove` takes it back because the file may be gone by then: the service
|
||||
* removes a reference only exactly, so removal names every passage that was indexed.
|
||||
*/
|
||||
export interface KnowledgePassageIndex {
|
||||
reference(scope: KnowledgeScope, fileId: string): Promise<string>
|
||||
index(
|
||||
scope: KnowledgeScope,
|
||||
input: {
|
||||
collectionName: string
|
||||
documentRef: string
|
||||
fileId: string
|
||||
passages: readonly KnowledgePassage[]
|
||||
signal?: AbortSignal
|
||||
},
|
||||
): Promise<void>
|
||||
remove(
|
||||
scope: KnowledgeScope,
|
||||
input: { documentRef: string; passages: ReadonlyArray<Pick<KnowledgePassage, "page" | "index">> },
|
||||
): Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* What the status read asks of the deployment's search backend. It is a port of its own because a read
|
||||
* handler may not resolve `KNOWLEDGE_SEARCH`, and must not build it: a factory runs with the resolving
|
||||
* handler's restrictions, so a search service built for a read would hold a read-only data source.
|
||||
*/
|
||||
export type KnowledgeReadiness = Pick<KnowledgeSearchService, "readiness">
|
||||
|
||||
/**
|
||||
* The enablement platform: the checks the enablement flow needs, over the platform's own APIs.
|
||||
* `checkEligibility` is where the plan capability and the rollout flags surface as typed reasons;
|
||||
* `indexingQuota` answers today's remaining indexing volume.
|
||||
*/
|
||||
export interface DocumentAiPlatform {
|
||||
checkEligibility(
|
||||
scope: KnowledgeOrgScope,
|
||||
): Promise<{ ok: true } | { ok: false; reason: EnablementReason; detail?: string }>
|
||||
indexingQuota(scope: KnowledgeOrgScope): Promise<number>
|
||||
}
|
||||
|
||||
/** What a flag service answers for the project a call runs in. */
|
||||
export interface KnowledgeFlags {
|
||||
enabled(flag: keyof typeof KNOWLEDGE_FLAG_DEFAULTS, scope: KnowledgeScope): boolean | Promise<boolean>
|
||||
}
|
||||
|
||||
/**
|
||||
* Bound by an app that evaluates flags (the platform's flag service). Unbound: knowledge is on for an
|
||||
* organization once an administrator has enabled document AI, and off until then (`enablementFlags`).
|
||||
*/
|
||||
export const KNOWLEDGE_FLAGS = token<KnowledgeFlags>("knowledge.flags")
|
||||
|
||||
/** The organization's own switch: the row `knowledge.settings.enableDocumentAi` writes. */
|
||||
export const enablementFlags = (data: DataSource): KnowledgeFlags => ({
|
||||
enabled: async (flag, scope) => {
|
||||
const row = (await data.forScope(scope).of(config).list({ limit: 1 })).items[0]
|
||||
return row?.enabled ?? KNOWLEDGE_FLAG_DEFAULTS[flag]
|
||||
},
|
||||
})
|
||||
|
||||
export const flagsOf = (container: Container, data: DataSource): KnowledgeFlags =>
|
||||
container.has(KNOWLEDGE_FLAGS) ? container.resolve(KNOWLEDGE_FLAGS) : enablementFlags(data)
|
||||
|
||||
/** Bound by the worker and the web shell with the module's search backend adapter. */
|
||||
export const KNOWLEDGE_SEARCH = token<KnowledgeSearchService>("knowledge.search", { access: "effect" })
|
||||
|
||||
/** Bound by the worker with the platform's document reader: the automatic PDF intake reads through it. */
|
||||
export const KNOWLEDGE_READER = token<KnowledgeReader>("knowledge.reader", { access: "effect" })
|
||||
|
||||
/** Bound by the worker with the knowledge service's inline index: the PDF intake's passages, and their removal. */
|
||||
export const KNOWLEDGE_PASSAGES = token<KnowledgePassageIndex>("knowledge.passages", { access: "effect" })
|
||||
|
||||
/** Bound by the web shell with the readiness of the service `KNOWLEDGE_SEARCH` serves. */
|
||||
export const KNOWLEDGE_READINESS = token<KnowledgeReadiness>("knowledge.readiness")
|
||||
|
||||
/**
|
||||
* Bound by the web shell (the enablement flow runs in a request) and by the worker (the PDF intake checks
|
||||
* eligibility and quota again before each read, core-11).
|
||||
*/
|
||||
export const KNOWLEDGE_ENABLEMENT = token<DocumentAiPlatform>("knowledge.enablement", { access: "effect" })
|
||||
|
||||
/** The per-run scope, under the module's own name (decision AT-F6). */
|
||||
export const KNOWLEDGE_SCOPE = token<TenantScope>("knowledge.scope")
|
||||
|
||||
export const knowledgeEnabled = async (deps: Pick<KnowledgeDeps, "flags">, scope: KnowledgeScope): Promise<boolean> =>
|
||||
deps.flags.enabled("knowledge.enabled", scope)
|
||||
Reference in New Issue
Block a user