Handle object-shaped changed sections
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
articleInputSchema,
|
||||
confirmedFactCardSchema,
|
||||
candidateFactCardSchema,
|
||||
optimizedArticleSchema,
|
||||
qaReportSchema,
|
||||
} from "../validation";
|
||||
|
||||
@@ -144,4 +145,23 @@ describe("domain validation", () => {
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("normalizes object-shaped changed sections from LLM output", () => {
|
||||
const parsed = optimizedArticleSchema.parse({
|
||||
title: "Optimized article",
|
||||
summary: "Summary constrained by the confirmed fact card.",
|
||||
body_markdown: "## Body\nUpdated body.",
|
||||
image_suggestions: [],
|
||||
changed_sections: [
|
||||
{ section: "title", change: "Improved keyword clarity." },
|
||||
{ name: "body", reason: "Fixed sentence flow." },
|
||||
],
|
||||
requires_user_confirmation: [],
|
||||
});
|
||||
|
||||
expect(parsed.changed_sections).toEqual([
|
||||
"title: Improved keyword clarity.",
|
||||
"body: Fixed sentence flow.",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -117,6 +117,60 @@ export const imageSuggestionSchema = z.object({
|
||||
suggestion: z.string().trim().min(1),
|
||||
});
|
||||
|
||||
function firstStringField(record: Record<string, unknown>, keys: string[]) {
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
if (typeof value === "string" && value.trim().length > 0) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeChangedSection(value: unknown) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const record = value as Record<string, unknown>;
|
||||
const section = firstStringField(record, [
|
||||
"section",
|
||||
"name",
|
||||
"title",
|
||||
"field",
|
||||
"target",
|
||||
"part",
|
||||
]);
|
||||
const detail = firstStringField(record, [
|
||||
"change",
|
||||
"changed",
|
||||
"summary",
|
||||
"reason",
|
||||
"description",
|
||||
"fix",
|
||||
"after",
|
||||
]);
|
||||
|
||||
if (section && detail && section !== detail) {
|
||||
return `${section}: ${detail}`;
|
||||
}
|
||||
if (section || detail) {
|
||||
return section ?? detail;
|
||||
}
|
||||
|
||||
const stringValues = Object.values(record)
|
||||
.filter((entry): entry is string => typeof entry === "string")
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return stringValues.join("; ");
|
||||
}
|
||||
|
||||
const changedSectionSchema = z.preprocess(
|
||||
normalizeChangedSection,
|
||||
z.string().trim().min(1),
|
||||
);
|
||||
|
||||
export const optimizedArticleSchema = z.object({
|
||||
job_id: z.string().trim().min(1).optional(),
|
||||
revision: z.number().int().positive().optional(),
|
||||
@@ -124,7 +178,7 @@ export const optimizedArticleSchema = z.object({
|
||||
summary: z.string().trim().min(1),
|
||||
body_markdown: z.string().trim().min(1),
|
||||
image_suggestions: z.array(imageSuggestionSchema).default([]),
|
||||
changed_sections: z.array(z.string().trim().min(1)).default([]),
|
||||
changed_sections: z.array(changedSectionSchema).default([]),
|
||||
requires_user_confirmation: z.array(z.string().trim().min(1)).default([]),
|
||||
}) satisfies z.ZodType<OptimizedArticle>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user