117 lines
4.3 KiB
TypeScript
117 lines
4.3 KiB
TypeScript
// 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 {
|
|
type FilesDeps,
|
|
findFileStorageByKey,
|
|
getFileStorage,
|
|
platformBlobPathname,
|
|
platformBlobStorageKey,
|
|
} from "@conusai-kit/files"
|
|
import { type Knowledge, KnowledgeError } from "@conusai-kit/platform"
|
|
import type { KnowledgeHit, KnowledgeScope, KnowledgeSearchService } from "../core/ports.js"
|
|
|
|
export function platformKnowledgeSearch(client: Knowledge, files: FilesDeps): KnowledgeSearchService {
|
|
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,
|
|
)
|
|
}
|
|
const documentRef = async (scope: KnowledgeScope, fileId: string): Promise<string> => {
|
|
const reference = await getFileStorage(files, scope, fileId)
|
|
return platformBlobPathname(scope, reference.storageKey)
|
|
}
|
|
|
|
return {
|
|
async readiness() {
|
|
try {
|
|
const answer = await client.readiness()
|
|
return { state: answer.state, reasons: [...answer.reasons] }
|
|
} catch (error) {
|
|
return {
|
|
state: "unavailable",
|
|
reasons: [error instanceof Error ? (error.message.split("\n")[0] ?? error.message) : "not ready"],
|
|
}
|
|
}
|
|
},
|
|
|
|
async index(scope, input) {
|
|
try {
|
|
const { jobId } = await client.index({
|
|
collection: input.collectionName,
|
|
documentRef: await documentRef(scope, input.fileId),
|
|
options: {
|
|
visibility: "public",
|
|
contentSha256: createHash("sha256").update(input.bytes).digest("hex"),
|
|
},
|
|
})
|
|
// 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 (;;) {
|
|
const job = await client.job(jobId)
|
|
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 new Promise((resolve) => setTimeout(resolve, 1_000))
|
|
}
|
|
} catch (error) {
|
|
return unwrap(error)
|
|
}
|
|
},
|
|
|
|
async search(scope, input) {
|
|
try {
|
|
const results = await client.search({
|
|
collection: input.collectionName,
|
|
query: input.query,
|
|
limit: input.limit,
|
|
})
|
|
const hits: KnowledgeHit[] = []
|
|
for (const [at, hit] of results.entries()) {
|
|
const storageKey = platformBlobStorageKey(scope, hit.documentRef)
|
|
if (storageKey === null) continue
|
|
const reference = await findFileStorageByKey(files, scope, storageKey)
|
|
if (reference === null) continue
|
|
hits.push({
|
|
fileId: reference.file.fileId,
|
|
filename: reference.file.filename,
|
|
collectionName: input.collectionName,
|
|
score: hit.score,
|
|
chunk: at + 1,
|
|
snippet: hit.snippet,
|
|
})
|
|
}
|
|
return hits
|
|
} catch (error) {
|
|
return unwrap(error)
|
|
}
|
|
},
|
|
|
|
async remove(scope, input) {
|
|
try {
|
|
await client.remove(await documentRef(scope, input.fileId))
|
|
} catch (error) {
|
|
return unwrap(error)
|
|
}
|
|
},
|
|
}
|
|
}
|