feat: use llm for article optimization

This commit is contained in:
Codex
2026-06-21 23:43:02 +08:00
parent 0c36f4b8a3
commit 9efbe1a354
2 changed files with 82 additions and 0 deletions
@@ -15,8 +15,25 @@ vi.mock("../../llm/client", async () => {
}); });
import { extractCandidateFactCard } from "../fact-extractor"; import { extractCandidateFactCard } from "../fact-extractor";
import { optimizeArticle } from "../article-optimizer";
describe("LLM workflow integration", () => { describe("LLM workflow integration", () => {
const confirmedFactCard = {
company_full_name: "Example Technology Co., Ltd.",
company_short_names: ["Example Tech"],
brand_names: ["Example"],
product_names: ["Example GEO"],
target_industry: "GEO optimization",
target_audience: "Marketing teams",
experience_years: 8,
core_claims: ["8 years of GEO optimization experience"],
forbidden_claims: ["industry first"],
image_topics: ["dashboard"],
uncertain_items: [],
is_ready_for_optimization: true,
confirmed_by_user: true,
} as const;
afterEach(() => { afterEach(() => {
llmMocks.generateValidatedJson.mockReset(); llmMocks.generateValidatedJson.mockReset();
}); });
@@ -64,4 +81,50 @@ describe("LLM workflow integration", () => {
expect(card.company_full_name).toBe("Fallback Technology Co., Ltd."); expect(card.company_full_name).toBe("Fallback Technology Co., Ltd.");
expect(card.experience_years).toBe(8); expect(card.experience_years).toBe(8);
}); });
it("uses LLM output for article optimization when valid", async () => {
llmMocks.generateValidatedJson.mockResolvedValueOnce({
title: "LLM Optimized GEO Article",
summary: "LLM summary constrained by the fact card.",
body_markdown: "## LLM Body\nExample Technology Co., Ltd. keeps claims factual.",
image_suggestions: [{ source: "image_1", suggestion: "Use dashboard." }],
changed_sections: ["title", "body"],
requires_user_confirmation: [],
});
const article = await optimizeArticle({
input: {
title: "Original",
body: "Example Technology Co., Ltd. has 8 years of GEO optimization experience.",
images: [{ type: "description", content: "dashboard" }],
platform: "official_site",
user_instructions: "",
},
factCard: confirmedFactCard,
});
expect(article.title).toBe("LLM Optimized GEO Article");
expect(article.body_markdown).toContain("LLM Body");
expect(llmMocks.generateValidatedJson).toHaveBeenCalledOnce();
});
it("falls back to deterministic article optimization when LLM returns null", async () => {
llmMocks.generateValidatedJson.mockResolvedValueOnce(null);
const article = await optimizeArticle({
input: {
title: "Original",
body: "Example Technology Co., Ltd. has 8 years of GEO optimization experience.",
images: [{ type: "description", content: "dashboard" }],
platform: "official_site",
user_instructions: "Say we have 99 patents.",
},
factCard: confirmedFactCard,
});
expect(article.title).toContain("GEO optimization Guide");
expect(article.requires_user_confirmation).toContain(
"Unsupported requested claim: 99 patents",
);
});
}); });
+19
View File
@@ -4,6 +4,11 @@ import type {
OptimizedArticle, OptimizedArticle,
} from "../domain/types"; } from "../domain/types";
import { optimizedArticleSchema } from "../domain/validation"; import { optimizedArticleSchema } from "../domain/validation";
import { generateValidatedJson } from "../llm/client";
import {
ARTICLE_OPTIMIZER_SYSTEM_PROMPT,
buildArticleOptimizerPrompt,
} from "../llm/prompts";
export interface OptimizeArticleInput { export interface OptimizeArticleInput {
input: ArticleInput; input: ArticleInput;
@@ -14,6 +19,20 @@ export async function optimizeArticle({
input, input,
factCard, factCard,
}: OptimizeArticleInput): Promise<OptimizedArticle> { }: OptimizeArticleInput): Promise<OptimizedArticle> {
const llmArticle = await generateValidatedJson({
schema: optimizedArticleSchema,
system: ARTICLE_OPTIMIZER_SYSTEM_PROMPT,
prompt: buildArticleOptimizerPrompt(input, factCard),
temperature: 0.2,
});
return llmArticle ?? optimizeArticleFallback({ input, factCard });
}
function optimizeArticleFallback({
input,
factCard,
}: OptimizeArticleInput): OptimizedArticle {
const unsupported = findUnsupportedInstructionClaims( const unsupported = findUnsupportedInstructionClaims(
input.user_instructions, input.user_instructions,
factCard, factCard,