This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// Node action for the documents pipeline. OCR, Blob and model calls happen here; every repository
|
||||
// change below is a single internal mutation/query and proposal creation remains atomic with its
|
||||
// outbox event.
|
||||
|
||||
"use node"
|
||||
|
||||
import { env } from "node:process"
|
||||
import { AppError, notFound } from "@conusai-kit/core"
|
||||
import { systemActor } from "@conusai-kit/core/worker"
|
||||
import { DEFAULT_FILES_POLICY, type ProcessorJobPayload, platformObjectStore } from "@conusai-kit/files"
|
||||
import { createDocumentReader, createModelGateway, createObjectStore } from "@conusai-kit/platform"
|
||||
import { makeFunctionReference } from "convex/server"
|
||||
import { v } from "convex/values"
|
||||
import { excludedPageClassNamesOf } from "../config/dist/documents.js"
|
||||
import { pageClassesOf } from "../modules/dist/documents/contract/constants.js"
|
||||
import type { DocumentScope } from "../modules/dist/documents/core/lines.js"
|
||||
import {
|
||||
type DocumentProcessRecords,
|
||||
type ProcessDocumentRow,
|
||||
processDocumentWithRecords,
|
||||
} from "../modules/dist/documents/core/process.js"
|
||||
import { modelLineExtractor, sdkDocumentOcr } from "../modules/dist/documents/index.js"
|
||||
import { internalAction } from "./_generated/server"
|
||||
|
||||
const required = (name: string): string => {
|
||||
const value = env[name]?.trim()
|
||||
if (!value) throw new Error(`Convex deployment is missing ${name}`)
|
||||
return value
|
||||
}
|
||||
|
||||
const serviceEnv = () => {
|
||||
const project = Number(required("CONUSAI_PROJECT_ID"))
|
||||
if (!Number.isSafeInteger(project) || project < 1)
|
||||
throw new Error("Convex deployment has an invalid CONUSAI_PROJECT_ID")
|
||||
return {
|
||||
CONUSAI_API_URL: required("CONUSAI_API_URL"),
|
||||
CONUSAI_API_TOKEN: required("CONUSAI_API_TOKEN"),
|
||||
CONUSAI_PROJECT_ID: String(project),
|
||||
}
|
||||
}
|
||||
|
||||
const platformBlobStore = (deployment: ReturnType<typeof serviceEnv>) =>
|
||||
platformObjectStore({ blob: createObjectStore(deployment) })
|
||||
|
||||
const queryRef = (name: string) => makeFunctionReference<"query">(name)
|
||||
const mutationRef = (name: string) => makeFunctionReference<"mutation">(name)
|
||||
|
||||
const documentScope = (ids: { orgId: string; workspaceId: string; projectId: string }): DocumentScope => ({
|
||||
level: "project",
|
||||
actor: systemActor("documents.ocr"),
|
||||
...ids,
|
||||
})
|
||||
|
||||
export const process = internalAction({
|
||||
args: {
|
||||
jobId: v.string(),
|
||||
attempt: v.number(),
|
||||
orgId: v.string(),
|
||||
workspaceId: v.string(),
|
||||
projectId: v.string(),
|
||||
payload: v.any(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const ids = { orgId: args.orgId, workspaceId: args.workspaceId, projectId: args.projectId }
|
||||
const scope = documentScope(ids)
|
||||
const deployment = serviceEnv()
|
||||
const store = platformBlobStore(deployment)
|
||||
const reader = sdkDocumentOcr(createDocumentReader(deployment))
|
||||
const model = env.AI_MODEL?.trim() || "anthropic/claude-sonnet-5"
|
||||
const excludedPageClassNames = excludedPageClassNamesOf()
|
||||
const extractor = modelLineExtractor({
|
||||
gateway: createModelGateway(deployment, { model }),
|
||||
model,
|
||||
...(excludedPageClassNames === undefined ? {} : { excludedPageClasses: pageClassesOf(excludedPageClassNames) }),
|
||||
})
|
||||
const payload = args.payload as ProcessorJobPayload
|
||||
const records: DocumentProcessRecords = {
|
||||
async getFile(_scope, fileId) {
|
||||
const file = await ctx.runQuery(queryRef("documents:fileInfo"), { ...ids, fileId })
|
||||
if (!file) throw notFound("no such file")
|
||||
return file as never
|
||||
},
|
||||
ensureProcessing: async (_scope, _file, job) =>
|
||||
(await ctx.runMutation(mutationRef("documents:ensureProcessing"), {
|
||||
...ids,
|
||||
fileId: job.fileId,
|
||||
storageKey: job.storageKey,
|
||||
sha256: job.sha256,
|
||||
})) as ProcessDocumentRow | undefined,
|
||||
setOcrJob: async (_scope, document, jobId) =>
|
||||
(await ctx.runMutation(mutationRef("documents:setOcrJob"), {
|
||||
...ids,
|
||||
documentId: document.id,
|
||||
expectedVersion: document.version,
|
||||
jobId,
|
||||
})) as ProcessDocumentRow,
|
||||
markFailed: async (_scope, document, code, finishedAt) => {
|
||||
await ctx.runMutation(mutationRef("documents:markFailed"), {
|
||||
...ids,
|
||||
documentId: document.id,
|
||||
expectedVersion: document.version,
|
||||
code,
|
||||
finishedAt,
|
||||
})
|
||||
},
|
||||
proposals: async (_scope, documentId) => {
|
||||
const lines = await ctx.runQuery(queryRef("documents:lineProposals"), { ...ids, documentId })
|
||||
return lines.map(({ lineKey }: { lineKey?: string }) => ({ change: { lineKey } })) as never
|
||||
},
|
||||
createProposal: async (_scope, input, idempotencyKey) => {
|
||||
await ctx.runMutation(mutationRef("documents:addLineProposal"), {
|
||||
...ids,
|
||||
documentId: input.targetRecordId,
|
||||
input,
|
||||
idempotencyKey,
|
||||
})
|
||||
},
|
||||
finish: async (_scope, document, result) => {
|
||||
await ctx.runMutation(mutationRef("documents:finish"), {
|
||||
...ids,
|
||||
documentId: document.id,
|
||||
expectedVersion: document.version,
|
||||
...result,
|
||||
})
|
||||
},
|
||||
}
|
||||
try {
|
||||
await processDocumentWithRecords(
|
||||
{
|
||||
store,
|
||||
ocr: reader,
|
||||
extractor,
|
||||
maxBytes: DEFAULT_FILES_POLICY.maxBytes,
|
||||
},
|
||||
scope,
|
||||
payload,
|
||||
records,
|
||||
)
|
||||
await ctx.runMutation(mutationRef("conusaiJobs:settleAction"), {
|
||||
jobId: args.jobId,
|
||||
attempt: args.attempt,
|
||||
})
|
||||
} catch (error) {
|
||||
const retryAfterSeconds =
|
||||
error instanceof AppError && error.problem.code === "rate_limited" ? error.problem.retryAfter : undefined
|
||||
const errorCode = error instanceof AppError ? error.problem.code : error instanceof Error ? error.name : "unknown"
|
||||
await ctx.runMutation(mutationRef("conusaiJobs:settleAction"), {
|
||||
jobId: args.jobId,
|
||||
attempt: args.attempt,
|
||||
error: `documents.ocr failed (${errorCode})`,
|
||||
...(retryAfterSeconds === undefined ? {} : { retryAfterSeconds }),
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user