From 9efbe1a35405e88a2ed84929f48dc13083534a47 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 16 Jun 2026 22:51:44 +0800 Subject: [PATCH] feat: use llm for article optimization --- .../__tests__/llm-integration.test.ts | 63 +++++++++++++++++++ src/lib/workflow/article-optimizer.ts | 19 ++++++ 2 files changed, 82 insertions(+) diff --git a/src/lib/workflow/__tests__/llm-integration.test.ts b/src/lib/workflow/__tests__/llm-integration.test.ts index c3ba730..3db02b0 100644 --- a/src/lib/workflow/__tests__/llm-integration.test.ts +++ b/src/lib/workflow/__tests__/llm-integration.test.ts @@ -15,8 +15,25 @@ vi.mock("../../llm/client", async () => { }); import { extractCandidateFactCard } from "../fact-extractor"; +import { optimizeArticle } from "../article-optimizer"; 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(() => { llmMocks.generateValidatedJson.mockReset(); }); @@ -64,4 +81,50 @@ describe("LLM workflow integration", () => { expect(card.company_full_name).toBe("Fallback Technology Co., Ltd."); 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", + ); + }); }); diff --git a/src/lib/workflow/article-optimizer.ts b/src/lib/workflow/article-optimizer.ts index 3b7cbf7..ee81ba9 100644 --- a/src/lib/workflow/article-optimizer.ts +++ b/src/lib/workflow/article-optimizer.ts @@ -4,6 +4,11 @@ import type { OptimizedArticle, } from "../domain/types"; import { optimizedArticleSchema } from "../domain/validation"; +import { generateValidatedJson } from "../llm/client"; +import { + ARTICLE_OPTIMIZER_SYSTEM_PROMPT, + buildArticleOptimizerPrompt, +} from "../llm/prompts"; export interface OptimizeArticleInput { input: ArticleInput; @@ -14,6 +19,20 @@ export async function optimizeArticle({ input, factCard, }: OptimizeArticleInput): Promise { + 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( input.user_instructions, factCard,