Files
HealthLog/src/lib/documents/ai-runs/run-support.ts
T
Marc-André Bombeck eda5776886 Run document summaries and filing suggestions in the background
The summary and suggest routes accept `Prefer: respond-async`: after the
same quick refusals they answer 202 with a run id, and the background
document AI worker runs the read under two new run kinds
(DOCUMENT_SUMMARY, DOCUMENT_SUGGEST, migration 0370). Without the header
both routes answer synchronously as before; both paths run one shared
body, so they cannot drift.

The worker asks the capability for the record and re-checks the wire for
the provider it picks before every read. A summary that is to be stored
marks the document PENDING while queued, READY when it lands, and
UNAVAILABLE when the run fails or the write does not land. A second
request attaches to a live run only when it would produce the same thing.

The vault asks for both in the background and shows the same calm
waiting line the index uses, worded so a transient read never promises
that leaving the page keeps it. The egress guard and the route inventory
now hold every run kind to its dispatcher re-check and its capability.
2026-10-03 20:51:57 +02:00

75 lines
2.3 KiB
TypeScript

/**
* Pieces the summary, suggest and extract run bodies share: the provider pick
* types, the failure shape, and the answer for a stored original that could
* not be turned into a provider input.
*
* Every failure here carries the status, message and `documents.inbound.*`
* code the synchronous route has always answered with, so the background run
* reports exactly what the request would have.
*/
import type {
requireDocumentTextProvider,
requireDocumentVisionProvider,
} from "@/lib/documents/provider-order";
import type { VisionInput } from "@/lib/documents/ai-route-support";
import type { AiRunOutcome } from "./types";
export type DocumentVisionPick = Awaited<
ReturnType<typeof requireDocumentVisionProvider>
>;
export type DocumentTextPick = Awaited<
ReturnType<typeof requireDocumentTextProvider>
>;
/** Who asked: a request (with its client address) or a background run. */
export interface RunOrigin {
ipAddress: string | null;
worker: boolean;
}
/** A failure in the shape the route would answer with. */
export const runFailure = (f: {
status: number;
message: string;
errorCode: string;
}): AiRunOutcome<never> => ({ ok: false, ...f });
/** The provider error every document read answers with (422 parse, 502 wire). */
export function unreadableDocument(parseError: boolean): AiRunOutcome<never> {
return runFailure({
status: parseError ? 422 : 502,
message: "Couldn't read the document. Try a clearer copy.",
errorCode: "documents.inbound.extractFailed",
});
}
/**
* A stored original that could not become a provider input, as the summary
* and suggest routes have always answered it.
*/
export function visionPreparationFailure(
reason: Extract<VisionInput, { ok: false }>["reason"],
): AiRunOutcome<never> {
if (reason === "pdfNeedsAnthropic") {
return runFailure({
status: 422,
message:
"PDF scanning needs a Claude vision provider; use local OCR instead.",
errorCode: "documents.inbound.pdfNeedsAnthropic",
});
}
if (reason === "fileType") {
return runFailure({
status: 422,
message: "This document can't be scanned. Use local OCR (text mode).",
errorCode: "documents.inbound.fileType",
});
}
return runFailure({
status: 422,
message: "Couldn't read the stored document.",
errorCode: "documents.inbound.extractFailed",
});
}