This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
// 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)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user