mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
Screen the document summary and split its policy by surface
The summary is model prose about the document and is contractually descriptive-only, but it was returned to the browser verbatim with no guard. Screen it against the insights contracts and let each caller apply its own policy: the on-demand route replaces the text with a short honest statement because a user is waiting on it, while the background job persists nothing -- its updateMany is guarded on a null summary, so a stored refusal would stamp the document permanently with no path to regenerate. Transcription stays unscreened by construction. It reproduces the user's own document verbatim; a discharge letter that says to increase a dose is the prescriber's instruction, and censoring it would delete the user's own record from their own view. Locale is required on the summary input type and absent from the transcription input so a caller cannot silently fall back to the English banks.
This commit is contained in:
1 parent
ca37fab63c
commit
97df5c4704
3 files changed
+140
-21
No files matched your search
@@ -43,6 +43,7 @@ import {
|
||||
import {
|
||||
DocumentDescribeError,
|
||||
runDocumentSummary,
|
||||
documentSummaryBlockedCopy,
|
||||
transcribeDocument,
|
||||
type DescribeInput,
|
||||
} from "@/lib/documents/describe";
|
||||
@@ -60,6 +61,9 @@ import {
|
||||
type DocumentSummaryMode,
|
||||
} from "@/lib/validations/inbound-documents";
|
||||
|
||||
import { resolveServerLocale } from "@/lib/i18n/server-locale";
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type RouteParams = { params: Promise<{ id: string }> };
|
||||
@@ -85,10 +89,14 @@ function resolveMode(request: NextRequest): DocumentSummaryMode {
|
||||
async function describe(
|
||||
mode: DocumentSummaryMode,
|
||||
input: DescribeInput,
|
||||
locale: Locale,
|
||||
): Promise<{ summary: string } | { text: string }> {
|
||||
return mode === "text"
|
||||
? transcribeDocument(input)
|
||||
: runDocumentSummary(input);
|
||||
if (mode === "text") return transcribeDocument(input);
|
||||
// REPLACE policy: the user clicked "summarise" and is waiting, so a screened
|
||||
// summary becomes a short honest statement rather than an empty panel. The
|
||||
// document itself and the extracted text remain reachable and unaltered.
|
||||
const { summary, blocked } = await runDocumentSummary({ ...input, locale });
|
||||
return { summary: blocked ? documentSummaryBlockedCopy(locale) : summary };
|
||||
}
|
||||
|
||||
/** The budget the requested mode charges. */
|
||||
@@ -114,11 +122,17 @@ export const POST = apiHandler(
|
||||
}
|
||||
|
||||
const mode = resolveMode(request);
|
||||
// The outbound screen on the summary needs the reader's locale to pick its
|
||||
// pattern banks; resolve it once here and thread it into both describe legs.
|
||||
const locale = await resolveServerLocale({
|
||||
request,
|
||||
userLocale: user.locale ?? null,
|
||||
});
|
||||
const contentType = request.headers.get("content-type") ?? "";
|
||||
if (contentType.includes("application/json")) {
|
||||
return handleTextSummary(request, user.id, document, mode);
|
||||
return handleTextSummary(request, user.id, document, mode, locale);
|
||||
}
|
||||
return handleVisionSummary(request, user.id, document, mode);
|
||||
return handleVisionSummary(request, user.id, document, mode, locale);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -148,6 +162,7 @@ async function handleTextSummary(
|
||||
userId: string,
|
||||
document: LoadedDocument,
|
||||
mode: DocumentSummaryMode,
|
||||
locale: Locale,
|
||||
): Promise<Response> {
|
||||
const row = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
@@ -214,11 +229,15 @@ async function handleTextSummary(
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await describe(mode, {
|
||||
provider: pick.entry.instance,
|
||||
providerType: pick.providerType,
|
||||
ocrText: parsed.data.text,
|
||||
});
|
||||
const result = await describe(
|
||||
mode,
|
||||
{
|
||||
provider: pick.entry.instance,
|
||||
providerType: pick.providerType,
|
||||
ocrText: parsed.data.text,
|
||||
},
|
||||
locale,
|
||||
);
|
||||
await reconcileSpend(
|
||||
userId,
|
||||
reservation.reserved,
|
||||
@@ -238,6 +257,7 @@ async function handleVisionSummary(
|
||||
userId: string,
|
||||
document: LoadedDocument,
|
||||
mode: DocumentSummaryMode,
|
||||
locale: Locale,
|
||||
): Promise<Response> {
|
||||
const { pick } = await resolveDocumentVisionProvider(userId);
|
||||
if (!pick) {
|
||||
@@ -293,12 +313,16 @@ async function handleVisionSummary(
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await describe(mode, {
|
||||
provider: pick.entry.instance,
|
||||
providerType: pick.providerType,
|
||||
images: vision.images,
|
||||
documents: vision.documents,
|
||||
});
|
||||
const result = await describe(
|
||||
mode,
|
||||
{
|
||||
provider: pick.entry.instance,
|
||||
providerType: pick.providerType,
|
||||
images: vision.images,
|
||||
documents: vision.documents,
|
||||
},
|
||||
locale,
|
||||
);
|
||||
await reconcileSpend(
|
||||
userId,
|
||||
reservation.reserved,
|
||||
|
||||
Reference in new issue
Block a user