接入文章优化案例自动保存
This commit is contained in:
@@ -30,6 +30,7 @@ import { GET as getJobProgress } from "../jobs/[jobId]/progress/route";
|
|||||||
import { POST as optimizeStream } from "../jobs/optimize-stream/route";
|
import { POST as optimizeStream } from "../jobs/optimize-stream/route";
|
||||||
import { POST as createJob } from "../jobs/route";
|
import { POST as createJob } from "../jobs/route";
|
||||||
import { POST as recordPerformance } from "../publications/[publicationId]/performance/route";
|
import { POST as recordPerformance } from "../publications/[publicationId]/performance/route";
|
||||||
|
import { createSqliteRepository } from "../../../lib/db/sqlite-repository";
|
||||||
|
|
||||||
const validFactCard = {
|
const validFactCard = {
|
||||||
company_full_name: "Example Technology Co., Ltd.",
|
company_full_name: "Example Technology Co., Ltd.",
|
||||||
@@ -89,6 +90,8 @@ interface StreamEventResponse {
|
|||||||
type: string;
|
type: string;
|
||||||
job_id?: string;
|
job_id?: string;
|
||||||
job?: { id: string };
|
job?: { id: string };
|
||||||
|
case?: { id: string; case_type: string };
|
||||||
|
result_version?: { id: string; version: number };
|
||||||
fact_card?: { company_full_name: string; confirmed_by_user?: boolean };
|
fact_card?: { company_full_name: string; confirmed_by_user?: boolean };
|
||||||
article?: { title: string; body_markdown?: string };
|
article?: { title: string; body_markdown?: string };
|
||||||
optimized_article?: { title: string };
|
optimized_article?: { title: string };
|
||||||
@@ -244,6 +247,44 @@ describe("job API routes", () => {
|
|||||||
).toBe("流式优化标题");
|
).toBe("流式优化标题");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("auto-saves stream article optimization as a case and result version", async () => {
|
||||||
|
llmMocks.generateValidatedJson
|
||||||
|
.mockResolvedValueOnce(validCandidateFactCard)
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
title: "流式优化标题",
|
||||||
|
summary: "流式优化摘要。",
|
||||||
|
body_markdown:
|
||||||
|
"## 服务能力\nExample Technology Co., Ltd. 提供 GEO optimization 服务。",
|
||||||
|
image_suggestions: [],
|
||||||
|
changed_sections: ["title", "body"],
|
||||||
|
requires_user_confirmation: [],
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({ checks: [] });
|
||||||
|
|
||||||
|
const response = await optimizeStream(
|
||||||
|
request({
|
||||||
|
body: "Example Technology Co., Ltd. has 8 years of GEO optimization experience.",
|
||||||
|
platform: "official_site",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const events = await streamEvents(response);
|
||||||
|
const finalEvent = events.find((event) => event.type === "final_ready");
|
||||||
|
|
||||||
|
expect(finalEvent?.case?.case_type).toBe("article");
|
||||||
|
expect(finalEvent?.result_version?.version).toBe(1);
|
||||||
|
|
||||||
|
const repository = createSqliteRepository();
|
||||||
|
await expect(
|
||||||
|
repository.listOptimizationCases({ include_archived: false }),
|
||||||
|
).resolves.toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: finalEvent?.case?.id,
|
||||||
|
case_type: "article",
|
||||||
|
status: "optimized",
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("uses an edited fact card without extracting a new one", async () => {
|
it("uses an edited fact card without extracting a new one", async () => {
|
||||||
llmMocks.generateValidatedJson
|
llmMocks.generateValidatedJson
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
@@ -325,6 +366,40 @@ describe("job API routes", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("auto-saves stream article LLM failure as a failed case", async () => {
|
||||||
|
llmMocks.generateValidatedJson
|
||||||
|
.mockResolvedValueOnce(validCandidateFactCard)
|
||||||
|
.mockRejectedValueOnce(new Error("LLM provider error: timeout"));
|
||||||
|
|
||||||
|
const response = await optimizeStream(
|
||||||
|
request({
|
||||||
|
body: "Example Technology Co., Ltd. has 8 years of GEO optimization experience.",
|
||||||
|
platform: "official_site",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const events = await streamEvents(response);
|
||||||
|
const failedEvent = events[events.length - 1];
|
||||||
|
|
||||||
|
expect(failedEvent.type).toBe("failed");
|
||||||
|
expect(failedEvent.case?.id).toMatch(/^case_/);
|
||||||
|
expect(failedEvent.result_version?.version).toBe(1);
|
||||||
|
|
||||||
|
const repository = createSqliteRepository();
|
||||||
|
const detail = await repository.getOptimizationCaseDetail(
|
||||||
|
failedEvent.case?.id ?? "",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(detail).toMatchObject({
|
||||||
|
case: { status: "failed", last_error_stage: "draft" },
|
||||||
|
versions: [
|
||||||
|
expect.objectContaining({
|
||||||
|
status: "failed",
|
||||||
|
error_summary: "LLM provider error: timeout",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects unresolved uncertain items when confirming a fact card", async () => {
|
it("rejects unresolved uncertain items when confirming a fact card", async () => {
|
||||||
const { job } = await createJobFixture();
|
const { job } = await createJobFixture();
|
||||||
const response = await confirmFactCard(
|
const response = await confirmFactCard(
|
||||||
@@ -387,6 +462,52 @@ describe("job API routes", () => {
|
|||||||
expect(body.timing.steps.every((step) => step.duration_ms >= 0)).toBe(true);
|
expect(body.timing.steps.every((step) => step.duration_ms >= 0)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("auto-saves non-stream article optimization as a case result version", async () => {
|
||||||
|
const { job } = await createJobFixture();
|
||||||
|
await confirmFactCard(
|
||||||
|
request(validFactCard),
|
||||||
|
params<{ jobId: string }>({ jobId: job.id }),
|
||||||
|
);
|
||||||
|
|
||||||
|
llmMocks.generateValidatedJson
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
title: "非流式优化标题",
|
||||||
|
summary:
|
||||||
|
"Example Technology Co., Ltd. 面向市场团队提供 GEO optimization 服务。",
|
||||||
|
body_markdown:
|
||||||
|
"Example Technology Co., Ltd. has 8 years of GEO optimization experience.",
|
||||||
|
image_suggestions: [],
|
||||||
|
changed_sections: ["title", "body"],
|
||||||
|
requires_user_confirmation: [],
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({ checks: [] });
|
||||||
|
|
||||||
|
const response = await optimizeJob(
|
||||||
|
request({}),
|
||||||
|
params<{ jobId: string }>({ jobId: job.id }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const repository = createSqliteRepository();
|
||||||
|
const savedJob = await repository.getArticleJob(job.id);
|
||||||
|
expect(savedJob?.case_id).toMatch(/^case_/);
|
||||||
|
|
||||||
|
const detail = await repository.getOptimizationCaseDetail(
|
||||||
|
savedJob?.case_id ?? "",
|
||||||
|
);
|
||||||
|
expect(detail).toMatchObject({
|
||||||
|
case: { case_type: "article", status: "optimized" },
|
||||||
|
versions: [
|
||||||
|
expect.objectContaining({
|
||||||
|
version: 1,
|
||||||
|
status: "optimized",
|
||||||
|
article_job_id: job.id,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("scores a revision, registers publication, and records manual performance", async () => {
|
it("scores a revision, registers publication, and records manual performance", async () => {
|
||||||
const { job } = await createJobFixture();
|
const { job } = await createJobFixture();
|
||||||
await confirmFactCard(
|
await confirmFactCard(
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
import { requireApiAccess } from "../../../../../lib/api/auth";
|
import { requireApiAccess } from "../../../../../lib/api/auth";
|
||||||
import { getRepositoryFromRuntime } from "../../../../../lib/db/repository";
|
import { buildArticleCaseSummary, createProcessStep } from "../../../../../lib/cases/summaries";
|
||||||
|
import { getRepositoryFromRuntime, type AppRepository } from "../../../../../lib/db/repository";
|
||||||
|
import type { ArticleJob } from "../../../../../lib/db/repositories";
|
||||||
|
import type { LlmAuditSummary } from "../../../../../lib/llm/audit";
|
||||||
import { LlmValidationError } from "../../../../../lib/llm/client";
|
import { LlmValidationError } from "../../../../../lib/llm/client";
|
||||||
import { getExportStoreFromRuntime } from "../../../../../lib/workflow/export-store";
|
import { getExportStoreFromRuntime } from "../../../../../lib/workflow/export-store";
|
||||||
import { runOptimizationWorkflow } from "../../../../../lib/workflow/orchestrator";
|
import { runOptimizationWorkflow } from "../../../../../lib/workflow/orchestrator";
|
||||||
@@ -36,6 +39,8 @@ export async function POST(request: Request, context: RouteContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const requestStartedAt = Date.now();
|
const requestStartedAt = Date.now();
|
||||||
|
const caseId = await ensureArticleCase(repository, job);
|
||||||
|
const llmAuditSummary: LlmAuditSummary[] = [];
|
||||||
startWorkflowProgress(jobId);
|
startWorkflowProgress(jobId);
|
||||||
try {
|
try {
|
||||||
const result = await runOptimizationWorkflow({
|
const result = await runOptimizationWorkflow({
|
||||||
@@ -50,6 +55,7 @@ export async function POST(request: Request, context: RouteContext) {
|
|||||||
onProgress: (event) => {
|
onProgress: (event) => {
|
||||||
recordWorkflowProgress(jobId, event);
|
recordWorkflowProgress(jobId, event);
|
||||||
},
|
},
|
||||||
|
onAuditSummary: (summary) => llmAuditSummary.push(summary),
|
||||||
});
|
});
|
||||||
const optimizedArticle = await repository.saveOptimizedArticle(
|
const optimizedArticle = await repository.saveOptimizedArticle(
|
||||||
jobId,
|
jobId,
|
||||||
@@ -70,8 +76,27 @@ export async function POST(request: Request, context: RouteContext) {
|
|||||||
status: "optimized",
|
status: "optimized",
|
||||||
export_paths: exportPaths,
|
export_paths: exportPaths,
|
||||||
});
|
});
|
||||||
|
const resultVersion = await repository.createOptimizationResultVersion({
|
||||||
|
case_id: caseId,
|
||||||
|
case_type: "article",
|
||||||
|
status: "optimized",
|
||||||
|
article_job_id: jobId,
|
||||||
|
article_revision: optimizedArticle.revision ?? 1,
|
||||||
|
result_summary: optimizedArticle.summary,
|
||||||
|
payload: {
|
||||||
|
article: optimizedArticle,
|
||||||
|
qa_report: qaReport,
|
||||||
|
export_paths: exportPaths,
|
||||||
|
},
|
||||||
|
process_summary: [],
|
||||||
|
llm_audit_summary: llmAuditSummary,
|
||||||
|
error_stage: null,
|
||||||
|
error_summary: null,
|
||||||
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
|
case: { id: caseId, case_type: "article" },
|
||||||
|
resultVersion: { id: resultVersion.id, version: resultVersion.version },
|
||||||
optimizedArticle,
|
optimizedArticle,
|
||||||
qaReport,
|
qaReport,
|
||||||
exportPaths,
|
exportPaths,
|
||||||
@@ -81,6 +106,28 @@ export async function POST(request: Request, context: RouteContext) {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "LLM optimization failed";
|
const message = error instanceof Error ? error.message : "LLM optimization failed";
|
||||||
|
await repository.createOptimizationResultVersion({
|
||||||
|
case_id: caseId,
|
||||||
|
case_type: "article",
|
||||||
|
status: "failed",
|
||||||
|
article_job_id: jobId,
|
||||||
|
article_revision: null,
|
||||||
|
result_summary: "",
|
||||||
|
payload: null,
|
||||||
|
process_summary: [
|
||||||
|
createProcessStep({
|
||||||
|
stage: "optimize",
|
||||||
|
startedAt: requestStartedAt,
|
||||||
|
endedAt: Date.now(),
|
||||||
|
status: "failed",
|
||||||
|
errorSummary: message,
|
||||||
|
producedResultVersion: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
llm_audit_summary: llmAuditSummary,
|
||||||
|
error_stage: "optimize",
|
||||||
|
error_summary: message,
|
||||||
|
});
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: message,
|
error: message,
|
||||||
@@ -94,6 +141,36 @@ export async function POST(request: Request, context: RouteContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureArticleCase(
|
||||||
|
repository: AppRepository,
|
||||||
|
job: ArticleJob,
|
||||||
|
) {
|
||||||
|
if (job.case_id) return job.case_id;
|
||||||
|
|
||||||
|
const caseSummary = buildArticleCaseSummary({
|
||||||
|
source_title: job.source_title,
|
||||||
|
source_body: job.source_body,
|
||||||
|
publish_platform: job.publish_platform,
|
||||||
|
});
|
||||||
|
const optimizationCase = await repository.createOptimizationCase({
|
||||||
|
case_type: "article",
|
||||||
|
...caseSummary,
|
||||||
|
});
|
||||||
|
await repository.saveCaseInput({
|
||||||
|
case_id: optimizationCase.id,
|
||||||
|
case_type: "article",
|
||||||
|
article_job_id: job.id,
|
||||||
|
payload: {
|
||||||
|
source_title: job.source_title,
|
||||||
|
source_body: job.source_body,
|
||||||
|
image_inputs: job.image_inputs,
|
||||||
|
publish_platform: job.publish_platform,
|
||||||
|
user_instructions: job.user_instructions,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return optimizationCase.id;
|
||||||
|
}
|
||||||
|
|
||||||
function getErrorStatus(error: unknown) {
|
function getErrorStatus(error: unknown) {
|
||||||
if (error instanceof LlmValidationError) return 502;
|
if (error instanceof LlmValidationError) return 502;
|
||||||
if (error instanceof Error && /^LLM\b|provider/i.test(error.message)) return 502;
|
if (error instanceof Error && /^LLM\b|provider/i.test(error.message)) return 502;
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
import { requireApiAccess } from "../../../../lib/api/auth";
|
import { requireApiAccess } from "../../../../lib/api/auth";
|
||||||
|
import type { ProcessSummaryStep } from "../../../../lib/cases/types";
|
||||||
|
import { buildArticleCaseSummary, createProcessStep } from "../../../../lib/cases/summaries";
|
||||||
import { getRepositoryFromRuntime } from "../../../../lib/db/repository";
|
import { getRepositoryFromRuntime } from "../../../../lib/db/repository";
|
||||||
import { optimizationFactCardSchema } from "../../../../lib/domain/validation";
|
import { optimizationFactCardSchema } from "../../../../lib/domain/validation";
|
||||||
|
import type { LlmAuditSummary } from "../../../../lib/llm/audit";
|
||||||
import { LlmValidationError } from "../../../../lib/llm/client";
|
import { LlmValidationError } from "../../../../lib/llm/client";
|
||||||
import { getExportStoreFromRuntime } from "../../../../lib/workflow/export-store";
|
import { getExportStoreFromRuntime } from "../../../../lib/workflow/export-store";
|
||||||
import { extractCandidateFactCard } from "../../../../lib/workflow/fact-extractor";
|
import { extractCandidateFactCard } from "../../../../lib/workflow/fact-extractor";
|
||||||
@@ -56,11 +59,26 @@ export async function POST(request: Request) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let jobId: string | undefined;
|
let jobId: string | undefined;
|
||||||
|
let caseId: string | undefined;
|
||||||
let stage: OptimizationStreamStage = "job";
|
let stage: OptimizationStreamStage = "job";
|
||||||
|
const llmAuditSummary: LlmAuditSummary[] = [];
|
||||||
|
const processSummary: ProcessSummaryStep[] = [];
|
||||||
|
const requestStartedAt = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const repository = getRepositoryFromRuntime();
|
const repository = getRepositoryFromRuntime();
|
||||||
|
const caseSummary = buildArticleCaseSummary({
|
||||||
|
source_title: normalized.articleInput.title,
|
||||||
|
source_body: normalized.articleInput.body,
|
||||||
|
publish_platform: normalized.articleInput.platform,
|
||||||
|
});
|
||||||
|
const optimizationCase = await repository.createOptimizationCase({
|
||||||
|
case_type: "article",
|
||||||
|
...caseSummary,
|
||||||
|
});
|
||||||
|
caseId = optimizationCase.id;
|
||||||
const job = await repository.createArticleJob({
|
const job = await repository.createArticleJob({
|
||||||
|
case_id: optimizationCase.id,
|
||||||
source_title: normalized.articleInput.title,
|
source_title: normalized.articleInput.title,
|
||||||
source_body: normalized.articleInput.body,
|
source_body: normalized.articleInput.body,
|
||||||
image_inputs: normalized.articleInput.images,
|
image_inputs: normalized.articleInput.images,
|
||||||
@@ -68,12 +86,40 @@ export async function POST(request: Request) {
|
|||||||
user_instructions: normalized.articleInput.user_instructions,
|
user_instructions: normalized.articleInput.user_instructions,
|
||||||
});
|
});
|
||||||
jobId = job.id;
|
jobId = job.id;
|
||||||
send({ type: "job_created", job: { id: job.id } });
|
await repository.saveCaseInput({
|
||||||
|
case_id: optimizationCase.id,
|
||||||
|
case_type: "article",
|
||||||
|
article_job_id: job.id,
|
||||||
|
payload: {
|
||||||
|
source_title: normalized.articleInput.title,
|
||||||
|
source_body: normalized.articleInput.body,
|
||||||
|
image_inputs: normalized.articleInput.images,
|
||||||
|
publish_platform: normalized.articleInput.platform,
|
||||||
|
user_instructions: normalized.articleInput.user_instructions,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
send({
|
||||||
|
type: "job_created",
|
||||||
|
job: { id: job.id },
|
||||||
|
case: { id: optimizationCase.id, case_type: "article" },
|
||||||
|
});
|
||||||
|
|
||||||
stage = "fact_card";
|
stage = "fact_card";
|
||||||
|
const factCardStartedAt = Date.now();
|
||||||
const factCard = optimizationFactCardSchema.parse(
|
const factCard = optimizationFactCardSchema.parse(
|
||||||
payload.fact_card ??
|
payload.fact_card ??
|
||||||
(await extractCandidateFactCard(normalized.articleInput)),
|
(await extractCandidateFactCard(normalized.articleInput, {
|
||||||
|
onAuditSummary: (summary) => llmAuditSummary.push(summary),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
processSummary.push(
|
||||||
|
createProcessStep({
|
||||||
|
stage: "fact_card",
|
||||||
|
startedAt: factCardStartedAt,
|
||||||
|
endedAt: Date.now(),
|
||||||
|
status: "success",
|
||||||
|
producedResultVersion: false,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
const savedFactCard = await repository.saveFactCard(job.id, factCard);
|
const savedFactCard = await repository.saveFactCard(job.id, factCard);
|
||||||
send({
|
send({
|
||||||
@@ -90,6 +136,7 @@ export async function POST(request: Request) {
|
|||||||
stage = stageForEvent(event, stage);
|
stage = stageForEvent(event, stage);
|
||||||
send(event);
|
send(event);
|
||||||
},
|
},
|
||||||
|
onAuditSummary: (summary) => llmAuditSummary.push(summary),
|
||||||
});
|
});
|
||||||
|
|
||||||
stage = "final";
|
stage = "final";
|
||||||
@@ -112,19 +159,75 @@ export async function POST(request: Request) {
|
|||||||
status: "optimized",
|
status: "optimized",
|
||||||
export_paths: exportPaths,
|
export_paths: exportPaths,
|
||||||
});
|
});
|
||||||
|
const resultVersion = await repository.createOptimizationResultVersion({
|
||||||
|
case_id: optimizationCase.id,
|
||||||
|
case_type: "article",
|
||||||
|
status: "optimized",
|
||||||
|
article_job_id: job.id,
|
||||||
|
article_revision: optimizedArticle.revision ?? 1,
|
||||||
|
result_summary: optimizedArticle.summary,
|
||||||
|
payload: {
|
||||||
|
article: optimizedArticle,
|
||||||
|
qa_report: qaReport,
|
||||||
|
export_paths: exportPaths,
|
||||||
|
},
|
||||||
|
process_summary: [...processSummary, ...result.processSummary],
|
||||||
|
llm_audit_summary: llmAuditSummary,
|
||||||
|
error_stage: null,
|
||||||
|
error_summary: null,
|
||||||
|
});
|
||||||
send({
|
send({
|
||||||
type: "final_ready",
|
type: "final_ready",
|
||||||
job_id: job.id,
|
job_id: job.id,
|
||||||
|
case: { id: optimizationCase.id, case_type: "article" },
|
||||||
|
result_version: { id: resultVersion.id, version: resultVersion.version },
|
||||||
optimized_article: optimizedArticle,
|
optimized_article: optimizedArticle,
|
||||||
qa_report: qaReport,
|
qa_report: qaReport,
|
||||||
export_paths: exportPaths,
|
export_paths: exportPaths,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "优化失败";
|
||||||
|
let failedVersion:
|
||||||
|
| { id: string; version: number }
|
||||||
|
| undefined;
|
||||||
|
if (caseId) {
|
||||||
|
try {
|
||||||
|
const repository = getRepositoryFromRuntime();
|
||||||
|
const version = await repository.createOptimizationResultVersion({
|
||||||
|
case_id: caseId,
|
||||||
|
case_type: "article",
|
||||||
|
status: "failed",
|
||||||
|
article_job_id: jobId ?? null,
|
||||||
|
article_revision: null,
|
||||||
|
result_summary: "",
|
||||||
|
payload: null,
|
||||||
|
process_summary: [
|
||||||
|
...processSummary,
|
||||||
|
createProcessStep({
|
||||||
|
stage,
|
||||||
|
startedAt: requestStartedAt,
|
||||||
|
endedAt: Date.now(),
|
||||||
|
status: "failed",
|
||||||
|
errorSummary: message,
|
||||||
|
producedResultVersion: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
llm_audit_summary: llmAuditSummary,
|
||||||
|
error_stage: stage,
|
||||||
|
error_summary: message,
|
||||||
|
});
|
||||||
|
failedVersion = { id: version.id, version: version.version };
|
||||||
|
} catch {
|
||||||
|
failedVersion = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
send({
|
send({
|
||||||
type: "failed",
|
type: "failed",
|
||||||
job_id: jobId,
|
job_id: jobId,
|
||||||
|
case: caseId ? { id: caseId, case_type: "article" } : undefined,
|
||||||
|
result_version: failedVersion,
|
||||||
stage,
|
stage,
|
||||||
error: error instanceof Error ? error.message : "优化失败",
|
error: message,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
controller.close();
|
controller.close();
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
import { requireApiAccess } from "../../../lib/api/auth";
|
import { requireApiAccess } from "../../../lib/api/auth";
|
||||||
|
import { buildArticleCaseSummary, createProcessStep } from "../../../lib/cases/summaries";
|
||||||
import { getRepositoryFromRuntime } from "../../../lib/db/repository";
|
import { getRepositoryFromRuntime } from "../../../lib/db/repository";
|
||||||
|
import type { LlmAuditSummary } from "../../../lib/llm/audit";
|
||||||
import { LlmValidationError } from "../../../lib/llm/client";
|
import { LlmValidationError } from "../../../lib/llm/client";
|
||||||
import { extractCandidateFactCard } from "../../../lib/workflow/fact-extractor";
|
import { extractCandidateFactCard } from "../../../lib/workflow/fact-extractor";
|
||||||
import { normalizeInput, type RawArticleInput } from "../../../lib/workflow/input-normalizer";
|
import { normalizeInput, type RawArticleInput } from "../../../lib/workflow/input-normalizer";
|
||||||
@@ -16,29 +18,78 @@ export async function POST(request: Request) {
|
|||||||
const payload = (await request.json()) as RawArticleInput;
|
const payload = (await request.json()) as RawArticleInput;
|
||||||
const normalized = normalizeInput(payload);
|
const normalized = normalizeInput(payload);
|
||||||
const repository = getRepositoryFromRuntime();
|
const repository = getRepositoryFromRuntime();
|
||||||
|
const caseSummary = buildArticleCaseSummary({
|
||||||
|
source_title: normalized.articleInput.title,
|
||||||
|
source_body: normalized.articleInput.body,
|
||||||
|
publish_platform: normalized.articleInput.platform,
|
||||||
|
});
|
||||||
|
const optimizationCase = await repository.createOptimizationCase({
|
||||||
|
case_type: "article",
|
||||||
|
...caseSummary,
|
||||||
|
});
|
||||||
const job = await repository.createArticleJob({
|
const job = await repository.createArticleJob({
|
||||||
|
case_id: optimizationCase.id,
|
||||||
source_title: normalized.articleInput.title,
|
source_title: normalized.articleInput.title,
|
||||||
source_body: normalized.articleInput.body,
|
source_body: normalized.articleInput.body,
|
||||||
image_inputs: normalized.articleInput.images,
|
image_inputs: normalized.articleInput.images,
|
||||||
publish_platform: normalized.articleInput.platform,
|
publish_platform: normalized.articleInput.platform,
|
||||||
user_instructions: normalized.articleInput.user_instructions,
|
user_instructions: normalized.articleInput.user_instructions,
|
||||||
});
|
});
|
||||||
|
await repository.saveCaseInput({
|
||||||
|
case_id: optimizationCase.id,
|
||||||
|
case_type: "article",
|
||||||
|
article_job_id: job.id,
|
||||||
|
payload: {
|
||||||
|
source_title: normalized.articleInput.title,
|
||||||
|
source_body: normalized.articleInput.body,
|
||||||
|
image_inputs: normalized.articleInput.images,
|
||||||
|
publish_platform: normalized.articleInput.platform,
|
||||||
|
user_instructions: normalized.articleInput.user_instructions,
|
||||||
|
},
|
||||||
|
});
|
||||||
const factStartedAt = Date.now();
|
const factStartedAt = Date.now();
|
||||||
|
const llmAuditSummary: LlmAuditSummary[] = [];
|
||||||
const timing = {
|
const timing = {
|
||||||
total_ms: 0,
|
total_ms: 0,
|
||||||
steps: [] as Array<{ label: string; duration_ms: number }>,
|
steps: [] as Array<{ label: string; duration_ms: number }>,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const candidateFactCard = await extractCandidateFactCard(normalized.articleInput);
|
const candidateFactCard = await extractCandidateFactCard(
|
||||||
|
normalized.articleInput,
|
||||||
|
{ onAuditSummary: (summary) => llmAuditSummary.push(summary) },
|
||||||
|
);
|
||||||
const duration = Date.now() - factStartedAt;
|
const duration = Date.now() - factStartedAt;
|
||||||
timing.total_ms = duration;
|
timing.total_ms = duration;
|
||||||
timing.steps.push({ label: "事实卡提取", duration_ms: duration });
|
timing.steps.push({ label: "事实卡提取", duration_ms: duration });
|
||||||
|
|
||||||
return NextResponse.json({ job, candidateFactCard, timing }, { status: 201 });
|
return NextResponse.json({ job, candidateFactCard, timing }, { status: 201 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Request failed";
|
||||||
const duration = Date.now() - factStartedAt;
|
const duration = Date.now() - factStartedAt;
|
||||||
timing.total_ms = duration;
|
timing.total_ms = duration;
|
||||||
timing.steps.push({ label: "事实卡提取", duration_ms: duration });
|
timing.steps.push({ label: "事实卡提取", duration_ms: duration });
|
||||||
|
await repository.createOptimizationResultVersion({
|
||||||
|
case_id: optimizationCase.id,
|
||||||
|
case_type: "article",
|
||||||
|
status: "failed",
|
||||||
|
article_job_id: job.id,
|
||||||
|
article_revision: null,
|
||||||
|
result_summary: "",
|
||||||
|
payload: null,
|
||||||
|
process_summary: [
|
||||||
|
createProcessStep({
|
||||||
|
stage: "fact_card",
|
||||||
|
startedAt: factStartedAt,
|
||||||
|
endedAt: Date.now(),
|
||||||
|
status: "failed",
|
||||||
|
errorSummary: message,
|
||||||
|
producedResultVersion: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
llm_audit_summary: llmAuditSummary,
|
||||||
|
error_stage: "fact_card",
|
||||||
|
error_summary: message,
|
||||||
|
});
|
||||||
return jsonError(error, getErrorStatus(error), timing);
|
return jsonError(error, getErrorStatus(error), timing);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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 { generateValidatedJson, type GenerateInput } from "../llm/client";
|
||||||
import {
|
import {
|
||||||
ARTICLE_OPTIMIZER_SYSTEM_PROMPT,
|
ARTICLE_OPTIMIZER_SYSTEM_PROMPT,
|
||||||
buildArticleOptimizerPrompt,
|
buildArticleOptimizerPrompt,
|
||||||
@@ -13,11 +13,13 @@ import {
|
|||||||
export interface OptimizeArticleInput {
|
export interface OptimizeArticleInput {
|
||||||
input: ArticleInput;
|
input: ArticleInput;
|
||||||
factCard: OptimizationFactCard;
|
factCard: OptimizationFactCard;
|
||||||
|
onAuditSummary?: GenerateInput["onAuditSummary"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function optimizeArticle({
|
export async function optimizeArticle({
|
||||||
input,
|
input,
|
||||||
factCard,
|
factCard,
|
||||||
|
onAuditSummary,
|
||||||
}: OptimizeArticleInput): Promise<OptimizedArticle> {
|
}: OptimizeArticleInput): Promise<OptimizedArticle> {
|
||||||
const llmArticle = await generateValidatedJson({
|
const llmArticle = await generateValidatedJson({
|
||||||
schema: optimizedArticleSchema,
|
schema: optimizedArticleSchema,
|
||||||
@@ -25,6 +27,7 @@ export async function optimizeArticle({
|
|||||||
prompt: buildArticleOptimizerPrompt(input, factCard),
|
prompt: buildArticleOptimizerPrompt(input, factCard),
|
||||||
temperature: 0.2,
|
temperature: 0.2,
|
||||||
task: "article_optimizer",
|
task: "article_optimizer",
|
||||||
|
onAuditSummary,
|
||||||
});
|
});
|
||||||
|
|
||||||
return optimizedArticleSchema.parse({
|
return optimizedArticleSchema.parse({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ArticleInput, CandidateFactCard } from "../domain/types";
|
import type { ArticleInput, CandidateFactCard } from "../domain/types";
|
||||||
import { candidateFactCardSchema } from "../domain/validation";
|
import { candidateFactCardSchema } from "../domain/validation";
|
||||||
import { generateValidatedJson } from "../llm/client";
|
import { generateValidatedJson, type GenerateInput } from "../llm/client";
|
||||||
import {
|
import {
|
||||||
FACT_EXTRACTOR_SYSTEM_PROMPT,
|
FACT_EXTRACTOR_SYSTEM_PROMPT,
|
||||||
buildFactExtractorPrompt,
|
buildFactExtractorPrompt,
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
|
|
||||||
export async function extractCandidateFactCard(
|
export async function extractCandidateFactCard(
|
||||||
input: ArticleInput,
|
input: ArticleInput,
|
||||||
|
options: { onAuditSummary?: GenerateInput["onAuditSummary"] } = {},
|
||||||
): Promise<CandidateFactCard> {
|
): Promise<CandidateFactCard> {
|
||||||
return generateValidatedJson({
|
return generateValidatedJson({
|
||||||
schema: candidateFactCardSchema,
|
schema: candidateFactCardSchema,
|
||||||
@@ -15,5 +16,6 @@ export async function extractCandidateFactCard(
|
|||||||
prompt: buildFactExtractorPrompt(input),
|
prompt: buildFactExtractorPrompt(input),
|
||||||
temperature: 0.1,
|
temperature: 0.1,
|
||||||
task: "fact_extractor",
|
task: "fact_extractor",
|
||||||
|
onAuditSummary: options.onAuditSummary,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ArticleInput, OptimizationFactCard } from "../domain/types";
|
import type { ArticleInput, OptimizationFactCard } from "../domain/types";
|
||||||
|
import type { GenerateInput } from "../llm/client";
|
||||||
|
|
||||||
import { optimizeArticle } from "./article-optimizer";
|
import { optimizeArticle } from "./article-optimizer";
|
||||||
import { inspectQualityWithLlm } from "./quality-inspector";
|
import { inspectQualityWithLlm } from "./quality-inspector";
|
||||||
@@ -8,6 +9,7 @@ export interface RunOptimizationWorkflowInput {
|
|||||||
input: ArticleInput;
|
input: ArticleInput;
|
||||||
factCard: OptimizationFactCard;
|
factCard: OptimizationFactCard;
|
||||||
onProgress?: (event: WorkflowProgressEvent) => void | Promise<void>;
|
onProgress?: (event: WorkflowProgressEvent) => void | Promise<void>;
|
||||||
|
onAuditSummary?: GenerateInput["onAuditSummary"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkflowTimingStep {
|
export interface WorkflowTimingStep {
|
||||||
@@ -68,13 +70,14 @@ export async function runOptimizationWorkflow({
|
|||||||
input,
|
input,
|
||||||
factCard,
|
factCard,
|
||||||
onProgress,
|
onProgress,
|
||||||
|
onAuditSummary,
|
||||||
}: RunOptimizationWorkflowInput) {
|
}: RunOptimizationWorkflowInput) {
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
const timingSteps: WorkflowTimingStep[] = [];
|
const timingSteps: WorkflowTimingStep[] = [];
|
||||||
let article = await timedStep(
|
let article = await timedStep(
|
||||||
"生成优化稿",
|
"生成优化稿",
|
||||||
timingSteps,
|
timingSteps,
|
||||||
() => optimizeArticle({ input, factCard }),
|
() => optimizeArticle({ input, factCard, onAuditSummary }),
|
||||||
onProgress,
|
onProgress,
|
||||||
);
|
);
|
||||||
let qaReport = await timedStep(
|
let qaReport = await timedStep(
|
||||||
@@ -86,6 +89,7 @@ export async function runOptimizationWorkflow({
|
|||||||
factCard,
|
factCard,
|
||||||
platform: input.platform,
|
platform: input.platform,
|
||||||
sourceImages: input.images,
|
sourceImages: input.images,
|
||||||
|
onAuditSummary,
|
||||||
}),
|
}),
|
||||||
onProgress,
|
onProgress,
|
||||||
);
|
);
|
||||||
@@ -97,7 +101,7 @@ export async function runOptimizationWorkflow({
|
|||||||
article = await timedStep(
|
article = await timedStep(
|
||||||
`定向修复第 ${nextRound} 轮`,
|
`定向修复第 ${nextRound} 轮`,
|
||||||
timingSteps,
|
timingSteps,
|
||||||
() => rewriteFailedSections({ article, factCard, failedChecks }),
|
() => rewriteFailedSections({ article, factCard, failedChecks, onAuditSummary }),
|
||||||
onProgress,
|
onProgress,
|
||||||
);
|
);
|
||||||
rewriteRounds = nextRound;
|
rewriteRounds = nextRound;
|
||||||
@@ -110,6 +114,7 @@ export async function runOptimizationWorkflow({
|
|||||||
factCard,
|
factCard,
|
||||||
platform: input.platform,
|
platform: input.platform,
|
||||||
sourceImages: input.images,
|
sourceImages: input.images,
|
||||||
|
onAuditSummary,
|
||||||
}),
|
}),
|
||||||
onProgress,
|
onProgress,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type {
|
|||||||
QualityRuleId,
|
QualityRuleId,
|
||||||
} from "../domain/types";
|
} from "../domain/types";
|
||||||
import { qaCheckSchema, qaReportSchema } from "../domain/validation";
|
import { qaCheckSchema, qaReportSchema } from "../domain/validation";
|
||||||
import { generateValidatedJson } from "../llm/client";
|
import { generateValidatedJson, type GenerateInput } from "../llm/client";
|
||||||
import {
|
import {
|
||||||
QUALITY_INSPECTOR_SYSTEM_PROMPT,
|
QUALITY_INSPECTOR_SYSTEM_PROMPT,
|
||||||
buildQualityInspectorPrompt,
|
buildQualityInspectorPrompt,
|
||||||
@@ -43,6 +43,7 @@ export interface InspectQualityInput {
|
|||||||
factCard: OptimizationFactCard;
|
factCard: OptimizationFactCard;
|
||||||
platform: PublishPlatform;
|
platform: PublishPlatform;
|
||||||
sourceImages: ImageInput[];
|
sourceImages: ImageInput[];
|
||||||
|
onAuditSummary?: GenerateInput["onAuditSummary"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inspectQuality(input: InspectQualityInput): QaReport {
|
export function inspectQuality(input: InspectQualityInput): QaReport {
|
||||||
@@ -71,6 +72,7 @@ export async function inspectQualityWithLlm(
|
|||||||
}),
|
}),
|
||||||
temperature: 0.1,
|
temperature: 0.1,
|
||||||
task: "quality_inspector",
|
task: "quality_inspector",
|
||||||
|
onAuditSummary: input.onAuditSummary,
|
||||||
});
|
});
|
||||||
|
|
||||||
const patchedChecks = deterministicReport.checks.map((deterministicCheck) => {
|
const patchedChecks = deterministicReport.checks.map((deterministicCheck) => {
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ export type OptimizationStreamStage =
|
|||||||
| "final";
|
| "final";
|
||||||
|
|
||||||
export type OptimizationStreamEvent =
|
export type OptimizationStreamEvent =
|
||||||
| { type: "job_created"; job: { id: string } }
|
| {
|
||||||
|
type: "job_created";
|
||||||
|
job: { id: string };
|
||||||
|
case?: { id: string; case_type: "article" };
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: "fact_card_ready";
|
type: "fact_card_ready";
|
||||||
job_id: string;
|
job_id: string;
|
||||||
@@ -34,6 +38,8 @@ export type OptimizationStreamEvent =
|
|||||||
| {
|
| {
|
||||||
type: "final_ready";
|
type: "final_ready";
|
||||||
job_id: string;
|
job_id: string;
|
||||||
|
case?: { id: string; case_type: "article" };
|
||||||
|
result_version?: { id: string; version: number };
|
||||||
optimized_article: OptimizedArticle;
|
optimized_article: OptimizedArticle;
|
||||||
qa_report: QaReport;
|
qa_report: QaReport;
|
||||||
export_paths: Record<string, string>;
|
export_paths: Record<string, string>;
|
||||||
@@ -41,6 +47,8 @@ export type OptimizationStreamEvent =
|
|||||||
| {
|
| {
|
||||||
type: "failed";
|
type: "failed";
|
||||||
job_id?: string;
|
job_id?: string;
|
||||||
|
case?: { id: string; case_type: "article" };
|
||||||
|
result_version?: { id: string; version: number };
|
||||||
stage: OptimizationStreamStage;
|
stage: OptimizationStreamStage;
|
||||||
error: string;
|
error: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import type { ArticleInput, OptimizationFactCard } from "../domain/types";
|
import type { ArticleInput, OptimizationFactCard } from "../domain/types";
|
||||||
|
import type { ProcessSummaryStep } from "../cases/types";
|
||||||
|
import { createProcessStep } from "../cases/summaries";
|
||||||
|
import type { GenerateInput } from "../llm/client";
|
||||||
|
|
||||||
import { optimizeArticle } from "./article-optimizer";
|
import { optimizeArticle } from "./article-optimizer";
|
||||||
import { inspectQualityWithLlm } from "./quality-inspector";
|
import { inspectQualityWithLlm } from "./quality-inspector";
|
||||||
@@ -10,6 +13,7 @@ export interface RunStreamingOptimizationWorkflowInput {
|
|||||||
input: ArticleInput;
|
input: ArticleInput;
|
||||||
factCard: OptimizationFactCard;
|
factCard: OptimizationFactCard;
|
||||||
onEvent: (event: OptimizationStreamEvent) => void | Promise<void>;
|
onEvent: (event: OptimizationStreamEvent) => void | Promise<void>;
|
||||||
|
onAuditSummary?: GenerateInput["onAuditSummary"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runStreamingOptimizationWorkflow({
|
export async function runStreamingOptimizationWorkflow({
|
||||||
@@ -17,13 +21,26 @@ export async function runStreamingOptimizationWorkflow({
|
|||||||
input,
|
input,
|
||||||
factCard,
|
factCard,
|
||||||
onEvent,
|
onEvent,
|
||||||
|
onAuditSummary,
|
||||||
}: RunStreamingOptimizationWorkflowInput) {
|
}: RunStreamingOptimizationWorkflowInput) {
|
||||||
|
const processSummary: ProcessSummaryStep[] = [];
|
||||||
|
|
||||||
await onEvent({
|
await onEvent({
|
||||||
type: "draft_started",
|
type: "draft_started",
|
||||||
job_id: jobId,
|
job_id: jobId,
|
||||||
message: "正在生成优化草稿",
|
message: "正在生成优化草稿",
|
||||||
});
|
});
|
||||||
let article = await optimizeArticle({ input, factCard });
|
let stageStartedAt = Date.now();
|
||||||
|
let article = await optimizeArticle({ input, factCard, onAuditSummary });
|
||||||
|
processSummary.push(
|
||||||
|
createProcessStep({
|
||||||
|
stage: "draft",
|
||||||
|
startedAt: stageStartedAt,
|
||||||
|
endedAt: Date.now(),
|
||||||
|
status: "success",
|
||||||
|
producedResultVersion: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
await onEvent({ type: "draft_ready", job_id: jobId, article });
|
await onEvent({ type: "draft_ready", job_id: jobId, article });
|
||||||
|
|
||||||
await onEvent({
|
await onEvent({
|
||||||
@@ -31,12 +48,23 @@ export async function runStreamingOptimizationWorkflow({
|
|||||||
job_id: jobId,
|
job_id: jobId,
|
||||||
message: "正在检查质量",
|
message: "正在检查质量",
|
||||||
});
|
});
|
||||||
|
stageStartedAt = Date.now();
|
||||||
let qaReport = await inspectQualityWithLlm({
|
let qaReport = await inspectQualityWithLlm({
|
||||||
article,
|
article,
|
||||||
factCard,
|
factCard,
|
||||||
platform: input.platform,
|
platform: input.platform,
|
||||||
sourceImages: input.images,
|
sourceImages: input.images,
|
||||||
|
onAuditSummary,
|
||||||
});
|
});
|
||||||
|
processSummary.push(
|
||||||
|
createProcessStep({
|
||||||
|
stage: "qa",
|
||||||
|
startedAt: stageStartedAt,
|
||||||
|
endedAt: Date.now(),
|
||||||
|
status: "success",
|
||||||
|
producedResultVersion: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
await onEvent({ type: "qa_ready", job_id: jobId, qa_report: qaReport });
|
await onEvent({ type: "qa_ready", job_id: jobId, qa_report: qaReport });
|
||||||
|
|
||||||
let rewriteRounds = 0;
|
let rewriteRounds = 0;
|
||||||
@@ -48,8 +76,24 @@ export async function runStreamingOptimizationWorkflow({
|
|||||||
job_id: jobId,
|
job_id: jobId,
|
||||||
round: nextRound,
|
round: nextRound,
|
||||||
});
|
});
|
||||||
article = await rewriteFailedSections({ article, factCard, failedChecks });
|
stageStartedAt = Date.now();
|
||||||
|
article = await rewriteFailedSections({
|
||||||
|
article,
|
||||||
|
factCard,
|
||||||
|
failedChecks,
|
||||||
|
onAuditSummary,
|
||||||
|
});
|
||||||
rewriteRounds = nextRound;
|
rewriteRounds = nextRound;
|
||||||
|
processSummary.push(
|
||||||
|
createProcessStep({
|
||||||
|
stage: "rewrite",
|
||||||
|
startedAt: stageStartedAt,
|
||||||
|
endedAt: Date.now(),
|
||||||
|
status: "success",
|
||||||
|
rewriteRound: nextRound,
|
||||||
|
producedResultVersion: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
await onEvent({
|
await onEvent({
|
||||||
type: "rewrite_ready",
|
type: "rewrite_ready",
|
||||||
job_id: jobId,
|
job_id: jobId,
|
||||||
@@ -62,12 +106,24 @@ export async function runStreamingOptimizationWorkflow({
|
|||||||
job_id: jobId,
|
job_id: jobId,
|
||||||
message: `正在复检第 ${nextRound} 轮修复`,
|
message: `正在复检第 ${nextRound} 轮修复`,
|
||||||
});
|
});
|
||||||
|
stageStartedAt = Date.now();
|
||||||
qaReport = await inspectQualityWithLlm({
|
qaReport = await inspectQualityWithLlm({
|
||||||
article,
|
article,
|
||||||
factCard,
|
factCard,
|
||||||
platform: input.platform,
|
platform: input.platform,
|
||||||
sourceImages: input.images,
|
sourceImages: input.images,
|
||||||
|
onAuditSummary,
|
||||||
});
|
});
|
||||||
|
processSummary.push(
|
||||||
|
createProcessStep({
|
||||||
|
stage: "qa",
|
||||||
|
startedAt: stageStartedAt,
|
||||||
|
endedAt: Date.now(),
|
||||||
|
status: "success",
|
||||||
|
rewriteRound: nextRound,
|
||||||
|
producedResultVersion: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
await onEvent({ type: "qa_ready", job_id: jobId, qa_report: qaReport });
|
await onEvent({ type: "qa_ready", job_id: jobId, qa_report: qaReport });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,5 +133,6 @@ export async function runStreamingOptimizationWorkflow({
|
|||||||
rewriteRounds,
|
rewriteRounds,
|
||||||
stoppedAfterMaxRewrites:
|
stoppedAfterMaxRewrites:
|
||||||
qaReport.overall_status === "fail" && rewriteRounds >= 2,
|
qaReport.overall_status === "fail" && rewriteRounds >= 2,
|
||||||
|
processSummary,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { OptimizationFactCard, OptimizedArticle, QaCheck } from "../domain/types";
|
import type { OptimizationFactCard, OptimizedArticle, QaCheck } from "../domain/types";
|
||||||
import { optimizedArticleSchema } from "../domain/validation";
|
import { optimizedArticleSchema } from "../domain/validation";
|
||||||
import { generateValidatedJson } from "../llm/client";
|
import { generateValidatedJson, type GenerateInput } from "../llm/client";
|
||||||
import {
|
import {
|
||||||
TARGETED_REWRITER_SYSTEM_PROMPT,
|
TARGETED_REWRITER_SYSTEM_PROMPT,
|
||||||
buildTargetedRewritePrompt,
|
buildTargetedRewritePrompt,
|
||||||
@@ -10,12 +10,14 @@ export interface RewriteFailedSectionsInput {
|
|||||||
article: OptimizedArticle;
|
article: OptimizedArticle;
|
||||||
factCard: OptimizationFactCard;
|
factCard: OptimizationFactCard;
|
||||||
failedChecks: QaCheck[];
|
failedChecks: QaCheck[];
|
||||||
|
onAuditSummary?: GenerateInput["onAuditSummary"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rewriteFailedSections({
|
export async function rewriteFailedSections({
|
||||||
article,
|
article,
|
||||||
factCard,
|
factCard,
|
||||||
failedChecks,
|
failedChecks,
|
||||||
|
onAuditSummary,
|
||||||
}: RewriteFailedSectionsInput): Promise<OptimizedArticle> {
|
}: RewriteFailedSectionsInput): Promise<OptimizedArticle> {
|
||||||
const llmArticle = await generateValidatedJson({
|
const llmArticle = await generateValidatedJson({
|
||||||
schema: optimizedArticleSchema,
|
schema: optimizedArticleSchema,
|
||||||
@@ -23,6 +25,7 @@ export async function rewriteFailedSections({
|
|||||||
prompt: buildTargetedRewritePrompt({ article, factCard, failedChecks }),
|
prompt: buildTargetedRewritePrompt({ article, factCard, failedChecks }),
|
||||||
temperature: 0.15,
|
temperature: 0.15,
|
||||||
task: "targeted_rewriter",
|
task: "targeted_rewriter",
|
||||||
|
onAuditSummary,
|
||||||
});
|
});
|
||||||
|
|
||||||
return optimizedArticleSchema.parse({
|
return optimizedArticleSchema.parse({
|
||||||
|
|||||||
Reference in New Issue
Block a user