fix(blog): anti-repetition engine — 6 angle types, forbidden structures, existing article context injection
This commit is contained in:
parent
957632c228
commit
7c8f545c18
@ -12,6 +12,8 @@
|
|||||||
* 8. Kill AI Tone (remove all AI fingerprints)
|
* 8. Kill AI Tone (remove all AI fingerprints)
|
||||||
* 9. QA Check (technical accuracy + weak section fixes)
|
* 9. QA Check (technical accuracy + weak section fixes)
|
||||||
* 10. Quality Score (1-10 ratings + improvement suggestions)
|
* 10. Quality Score (1-10 ratings + improvement suggestions)
|
||||||
|
* + APM (Auto-Precision Mode — final word-level cut)
|
||||||
|
* + Viral Signal (Social Masterfile — AVC, carry line, auto-kill, LinkedIn)
|
||||||
*
|
*
|
||||||
* Dedicated FO_Blog_LLM:
|
* Dedicated FO_Blog_LLM:
|
||||||
* - Model: qwen2.5:14b on .213 (or override via FO_BLOG_MODEL env)
|
* - Model: qwen2.5:14b on .213 (or override via FO_BLOG_MODEL env)
|
||||||
@ -303,14 +305,23 @@ CONTENT MODULES (use 2-3 per article):
|
|||||||
|
|
||||||
export const STEP1_TOPIC_EXPANSION = `You are a senior network engineer.
|
export const STEP1_TOPIC_EXPANSION = `You are a senior network engineer.
|
||||||
|
|
||||||
Given the topic below, expand it into:
|
Given the topic below, expand it into scenarios and angles from SIX DIFFERENT PERSPECTIVES.
|
||||||
- 5 real-world scenarios where this topic becomes a problem
|
Do NOT default to "physical layer failure" or "lab-to-production gap" — those are overused.
|
||||||
- 5 common wrong assumptions engineers make about this
|
|
||||||
- 5 operational risks nobody talks about
|
For each perspective, generate 2-3 concrete, specific observations:
|
||||||
|
|
||||||
|
1. ECONOMIC PERSPECTIVE — TCO, hidden costs, budget allocation, vendor pricing, ROI reality
|
||||||
|
2. OPERATIONAL PERSPECTIVE — procurement workflows, validation processes, team skills, SLAs
|
||||||
|
3. MARKET/TIMING PERSPECTIVE — when to buy, when to wait, what's mature vs. hype
|
||||||
|
4. TECHNICAL DEPTH PERSPECTIVE — specific failure modes at the protocol level, not just physical layer
|
||||||
|
5. POLITICAL PERSPECTIVE — vendor lock-in, procurement decisions, OEM vs. compatible debates
|
||||||
|
6. MIGRATION PERSPECTIVE — step-by-step realities when upgrading from previous generation
|
||||||
|
|
||||||
Topic: {{TOPIC}}
|
Topic: {{TOPIC}}
|
||||||
|
|
||||||
Keep it practical, not theoretical. Think about what actually goes wrong in production.`;
|
{{EXISTING_ANGLES}}
|
||||||
|
|
||||||
|
Be concrete, not generic. Think: what would a senior engineer with budget responsibility know that a junior engineer wouldn't?`;
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
// STEP 2: ANGLE SELECTION
|
// STEP 2: ANGLE SELECTION
|
||||||
@ -318,16 +329,46 @@ Keep it practical, not theoretical. Think about what actually goes wrong in prod
|
|||||||
|
|
||||||
export const STEP2_ANGLE_SELECTION = `Based on the expanded scenarios below, select ONE strong angle for a technical blog post.
|
export const STEP2_ANGLE_SELECTION = `Based on the expanded scenarios below, select ONE strong angle for a technical blog post.
|
||||||
|
|
||||||
The angle must be:
|
════════════════════════════════════════════════════════
|
||||||
- Practical and decision-driven (helps the reader DO something)
|
ANGLE DIVERSITY — MANDATORY
|
||||||
- Involves real trade-offs (not a clear-cut answer)
|
════════════════════════════════════════════════════════
|
||||||
- Relevant for real deployments (not academic)
|
|
||||||
- Controversial enough to generate discussion
|
|
||||||
|
|
||||||
Then define:
|
{{FORBIDDEN_ANGLES}}
|
||||||
- Target audience (e.g., DC leaf-spine engineer, ISP architect, enterprise campus)
|
|
||||||
- Core decision question the article answers
|
ANGLE TYPES TO CHOOSE FROM (pick the one that fits the topic best — rotate through these):
|
||||||
- The one thing the reader should DO after reading
|
|
||||||
|
TYPE A — ECONOMIC: "What this actually costs" (TCO, hidden spend, budget reality)
|
||||||
|
Example: "Your $350 optic just cost you $18,000 in engineering time — here's the math"
|
||||||
|
|
||||||
|
TYPE B — DECISION FRAMEWORK: "How to decide" (buy now vs wait, OEM vs compatible, which spec)
|
||||||
|
Example: "The 3 questions that determine whether 400G ZR is right for your deployment"
|
||||||
|
|
||||||
|
TYPE C — MARKET REALITY: "What's hype vs production-ready right now" (timing, maturity, supply chain)
|
||||||
|
Example: "800G: which parts of the standard are actually shippable today"
|
||||||
|
|
||||||
|
TYPE D — OPERATIONAL PLAYBOOK: "Step-by-step process" (how to do something, not what goes wrong)
|
||||||
|
Example: "The 6-step validation checklist before you deploy any 400G transceiver"
|
||||||
|
|
||||||
|
TYPE E — VENDOR POLITICS: "The uncomfortable truth about vendor dynamics"
|
||||||
|
Example: "Why OEM compatibility lists exist — and why they're not what you think"
|
||||||
|
|
||||||
|
TYPE F — MIGRATION REALITY: "What the upgrade path actually looks like, not what the datasheet says"
|
||||||
|
Example: "12 months into our 100G→400G migration: what we got wrong in month 1"
|
||||||
|
|
||||||
|
════════════════════════════════════════════════════════
|
||||||
|
FORBIDDEN ANGLE STRUCTURES (these are overused — auto-reject if you start here):
|
||||||
|
- "Lab worked fine → production failed → physical layer was the cause" → BANNED
|
||||||
|
- "Compatible optics get blamed → investigation → connector was dirty" → BANNED
|
||||||
|
- "400G exposes assumptions that 100G hid" → BANNED (used too many times)
|
||||||
|
- Any structure where the resolution is "clean your connectors" → BANNED
|
||||||
|
════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
Select the angle type, then define:
|
||||||
|
- ANGLE TYPE: (A/B/C/D/E/F)
|
||||||
|
- ANGLE SUMMARY: one sentence describing the specific angle
|
||||||
|
- TARGET AUDIENCE: (e.g., DC leaf-spine engineer with budget, ISP procurement lead, enterprise campus architect)
|
||||||
|
- CORE QUESTION: the specific decision or insight the article answers
|
||||||
|
- READER ACTION: the one thing the reader does differently after reading
|
||||||
|
|
||||||
Expanded scenarios:
|
Expanded scenarios:
|
||||||
{{SCENARIOS}}`;
|
{{SCENARIOS}}`;
|
||||||
@ -342,13 +383,21 @@ NOT a section list. NOT a structure. A flow plan — the sequence of ideas as th
|
|||||||
|
|
||||||
FORMAT: Write the outline as 3-4 narrative beats. Each beat = one core idea and how it connects to the next. No bullet points. No section headers.
|
FORMAT: Write the outline as 3-4 narrative beats. Each beat = one core idea and how it connects to the next. No bullet points. No section headers.
|
||||||
|
|
||||||
The outline should describe:
|
════════════════════════════════════════════════════════
|
||||||
- Opening situation: what moment the reader is in
|
BANNED OUTLINE STRUCTURES — DO NOT USE THESE:
|
||||||
- Core tension: what assumption they have that is wrong
|
- Beat 1: "Lab works fine" → Beat 2: "Production fails" → Beat 3: "Physical layer was the cause"
|
||||||
- Production reality: 1-2 specific things that fail (described as moments, not scenarios)
|
- Any arc where the climax is "dirty connector" or "polarity mismatch"
|
||||||
- Consequence/resolution: what actually matters at the end
|
- Any arc where the resolution is "validate your setup" as a generic close
|
||||||
|
- Opening with an engineer working late in a DC finding a failing link
|
||||||
|
════════════════════════════════════════════════════════
|
||||||
|
|
||||||
Keep the outline focused on 3-4 ideas MAX. If you can't write it in 3-4 beats, it's too broad.
|
INSTEAD — match the outline structure to the angle type:
|
||||||
|
- ECONOMIC angle → open with a cost moment, close with a calculation the reader can use
|
||||||
|
- DECISION angle → open with the choice the reader is about to make, close with clear criteria
|
||||||
|
- MARKET angle → open with what the market says vs. what the data shows, close with timing advice
|
||||||
|
- OPERATIONAL angle → open with a process gap, close with a concrete improved process
|
||||||
|
- POLITICAL angle → open with the vendor dynamic, close with what independence actually costs/saves
|
||||||
|
- MIGRATION angle → open with the planning assumption, close with what month 6 actually looked like
|
||||||
|
|
||||||
Angle: {{ANGLE}}
|
Angle: {{ANGLE}}
|
||||||
Target audience: {{AUDIENCE}}
|
Target audience: {{AUDIENCE}}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import { pool } from "../db/client";
|
|||||||
const pipelineProgress = new Map<string, { step: number; total: number; label: string; pct: number }>();
|
const pipelineProgress = new Map<string, { step: number; total: number; label: string; pct: number }>();
|
||||||
|
|
||||||
function setProgress(draftId: string, step: number, label: string): void {
|
function setProgress(draftId: string, step: number, label: string): void {
|
||||||
const pct = Math.round((step / 17) * 92) + 2; // 2%..94% during run, 100% on complete
|
const pct = Math.round((step / 18) * 92) + 2; // 2%..94% during run, 100% on complete
|
||||||
pipelineProgress.set(draftId, { step, total: 17, label, pct });
|
pipelineProgress.set(draftId, { step, total: 17, label, pct });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,6 +31,7 @@ import {
|
|||||||
ANTI_GENERIC_INTRO_PROMPT,
|
ANTI_GENERIC_INTRO_PROMPT,
|
||||||
QUALITY_CONTROL_PROMPT,
|
QUALITY_CONTROL_PROMPT,
|
||||||
PROCUREMENT_LAYER_PROMPT,
|
PROCUREMENT_LAYER_PROMPT,
|
||||||
|
VIRAL_SIGNAL_PROMPT,
|
||||||
buildTopicPrompt,
|
buildTopicPrompt,
|
||||||
} from "../llm/blog-prompts";
|
} from "../llm/blog-prompts";
|
||||||
|
|
||||||
@ -1019,7 +1020,7 @@ async function runLlmPipeline(
|
|||||||
|
|
||||||
const LLM_OPTS = { temperature: 0.7, maxTokens: 8192, timeoutMs: 480000 };
|
const LLM_OPTS = { temperature: 0.7, maxTokens: 8192, timeoutMs: 480000 };
|
||||||
const LLM_REFINE = { temperature: 0.4, maxTokens: 6144, timeoutMs: 480000 };
|
const LLM_REFINE = { temperature: 0.4, maxTokens: 6144, timeoutMs: 480000 };
|
||||||
const TOTAL_STEPS = 17; // 16-step pipeline + APM final cut
|
const TOTAL_STEPS = 18; // 16-step pipeline + APM + Viral Signal + LinkedIn
|
||||||
let stepsCompleted = 0;
|
let stepsCompleted = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -1097,11 +1098,35 @@ async function runLlmPipeline(
|
|||||||
// Get blog type config
|
// Get blog type config
|
||||||
const blogType = BLOG_TYPES[selectedTopic as keyof typeof BLOG_TYPES] || BLOG_TYPES.tutorial;
|
const blogType = BLOG_TYPES[selectedTopic as keyof typeof BLOG_TYPES] || BLOG_TYPES.tutorial;
|
||||||
|
|
||||||
|
// Load existing articles to prevent angle repetition
|
||||||
|
let existingAnglesContext = "";
|
||||||
|
let forbiddenAnglesContext = "";
|
||||||
|
try {
|
||||||
|
const existingResult = await pool.query(
|
||||||
|
`SELECT title, draft_content FROM blog_drafts
|
||||||
|
WHERE status IN ('published', 'review', 'ready') AND draft_content IS NOT NULL
|
||||||
|
ORDER BY created_at DESC LIMIT 10`
|
||||||
|
);
|
||||||
|
if (existingResult.rows.length > 0) {
|
||||||
|
const summaries = existingResult.rows.map((r: { title: string; draft_content: string }) => {
|
||||||
|
// Extract first 150 chars of content as summary
|
||||||
|
const preview = (r.draft_content || "").replace(/^#[^\n]*\n/, "").trim().slice(0, 150);
|
||||||
|
return `- "${r.title}": ${preview}...`;
|
||||||
|
}).join("\n");
|
||||||
|
|
||||||
|
existingAnglesContext = `\n\nALREADY PUBLISHED ARTICLES (do NOT repeat their angles or structure):\n${summaries}\n\nFor this new article, choose a COMPLETELY DIFFERENT perspective and angle than any of the above.`;
|
||||||
|
|
||||||
|
forbiddenAnglesContext = `ALREADY WRITTEN ANGLES (forbidden — do not repeat these):\n${existingResult.rows.map((r: { title: string }) => `- "${r.title}"`).join("\n")}\n\nThe new article MUST have a structurally different angle — different story type, different reader takeaway, different perspective lens.\n`;
|
||||||
|
}
|
||||||
|
} catch { /* fine if no articles yet */ }
|
||||||
|
|
||||||
// ═══ STEP 1: Topic Expansion ═══
|
// ═══ STEP 1: Topic Expansion ═══
|
||||||
console.log(" Step 1/10: Topic Expansion...");
|
console.log(" Step 1/10: Topic Expansion...");
|
||||||
setProgress(draftId, 1, "Step 1/10: Topic Expansion");
|
setProgress(draftId, 1, "Step 1/10: Topic Expansion");
|
||||||
const step1 = await generate(systemPrompt,
|
const step1 = await generate(systemPrompt,
|
||||||
STEP1_TOPIC_EXPANSION.replace("{{TOPIC}}", title),
|
STEP1_TOPIC_EXPANSION
|
||||||
|
.replace("{{TOPIC}}", title)
|
||||||
|
.replace("{{EXISTING_ANGLES}}", existingAnglesContext),
|
||||||
LLM_OPTS
|
LLM_OPTS
|
||||||
);
|
);
|
||||||
stepsCompleted = 1;
|
stepsCompleted = 1;
|
||||||
@ -1110,7 +1135,9 @@ async function runLlmPipeline(
|
|||||||
console.log(" Step 2/10: Angle Selection...");
|
console.log(" Step 2/10: Angle Selection...");
|
||||||
setProgress(draftId, 2, "Step 2/10: Angle Selection");
|
setProgress(draftId, 2, "Step 2/10: Angle Selection");
|
||||||
const step2 = await generate(systemPrompt,
|
const step2 = await generate(systemPrompt,
|
||||||
STEP2_ANGLE_SELECTION.replace("{{SCENARIOS}}", step1.text),
|
STEP2_ANGLE_SELECTION
|
||||||
|
.replace("{{FORBIDDEN_ANGLES}}", forbiddenAnglesContext)
|
||||||
|
.replace("{{SCENARIOS}}", step1.text),
|
||||||
LLM_REFINE
|
LLM_REFINE
|
||||||
);
|
);
|
||||||
stepsCompleted = 2;
|
stepsCompleted = 2;
|
||||||
@ -1259,8 +1286,8 @@ async function runLlmPipeline(
|
|||||||
stepsCompleted = 15;
|
stepsCompleted = 15;
|
||||||
|
|
||||||
// ═══ STEP APM: Auto-Precision Mode (Final Cut — last filter before publish) ═══
|
// ═══ STEP APM: Auto-Precision Mode (Final Cut — last filter before publish) ═══
|
||||||
console.log(" Step 16/17: Auto-Precision Mode (final cut — if a word can go, it must go)...");
|
console.log(" Step 16/18: Auto-Precision Mode (final cut — if a word can go, it must go)...");
|
||||||
setProgress(draftId, 16, "Step 16/17: Auto-Precision Mode");
|
setProgress(draftId, 16, "Step 16/18: Auto-Precision Mode");
|
||||||
const stepAPM = await generate(systemPrompt,
|
const stepAPM = await generate(systemPrompt,
|
||||||
STEP_APM.replace("{{ARTICLE}}", step9.text),
|
STEP_APM.replace("{{ARTICLE}}", step9.text),
|
||||||
LLM_REFINE
|
LLM_REFINE
|
||||||
@ -1271,34 +1298,72 @@ async function runLlmPipeline(
|
|||||||
const pctAPM = Math.round((1 - wordsAPM / wordsBeforeAPM) * 100);
|
const pctAPM = Math.round((1 - wordsAPM / wordsBeforeAPM) * 100);
|
||||||
console.log(` APM: ${wordsBeforeAPM} → ${wordsAPM} words (−${pctAPM}%) — precision cut done`);
|
console.log(` APM: ${wordsBeforeAPM} → ${wordsAPM} words (−${pctAPM}%) — precision cut done`);
|
||||||
|
|
||||||
// ═══ LinkedIn Post ═══
|
// ═══ STEP 17: Viral Signal — FLEXOPTIX Social Masterfile transformation ═══
|
||||||
console.log(" Step 17/17: LinkedIn Post (max 2,800 chars)...");
|
// Applies AVC (Auto-Viral-Check), ASS (Auto-Signal-Score), carry line enforcement,
|
||||||
setProgress(draftId, 17, "Step 17/17: LinkedIn Post");
|
// auto-kill phrase filter, and generates LinkedIn post in one pass.
|
||||||
let linkedinPost: string | null = null;
|
console.log(" Step 17/18: Viral Signal (Social Masterfile transformation)...");
|
||||||
let linkedinCharCount: number | null = null;
|
setProgress(draftId, 17, "Step 17/18: Viral Signal");
|
||||||
|
let viralArticle = stepAPM.text;
|
||||||
|
let viralLinkedinPost: string | null = null;
|
||||||
try {
|
try {
|
||||||
const stepLinkedIn = await generate(systemPrompt,
|
const stepViral = await generate(systemPrompt,
|
||||||
STEP_LINKEDIN_POST.replace("{{ARTICLE}}", stepAPM.text),
|
VIRAL_SIGNAL_PROMPT + "\n\nArticle:\n" + stepAPM.text,
|
||||||
{ temperature: 0.6, maxTokens: 1024, timeoutMs: 120000 }
|
{ temperature: 0.5, maxTokens: 8192, timeoutMs: 480000 }
|
||||||
);
|
);
|
||||||
linkedinPost = stepLinkedIn.text.trim();
|
const viralOutput = stepViral.text.trim();
|
||||||
linkedinCharCount = linkedinPost.length;
|
// Parse output: article + ---LINKEDIN--- + linkedin post
|
||||||
// Enforce hard limit — truncate at last sentence before 2800 if too long
|
const linkedinSep = viralOutput.indexOf("---LINKEDIN---");
|
||||||
if (linkedinCharCount > 2800) {
|
if (linkedinSep !== -1) {
|
||||||
linkedinPost = linkedinPost.slice(0, 2800).replace(/[^.!?]*$/, "").trim();
|
viralArticle = viralOutput.slice(0, linkedinSep).trim();
|
||||||
linkedinCharCount = linkedinPost.length;
|
viralLinkedinPost = viralOutput.slice(linkedinSep + "---LINKEDIN---".length).trim();
|
||||||
console.log(` LinkedIn post truncated to ${linkedinCharCount} chars`);
|
console.log(` Viral Signal: article ${viralArticle.split(/\s+/).length} words + LinkedIn ${viralLinkedinPost.length} chars`);
|
||||||
} else {
|
} else {
|
||||||
console.log(` LinkedIn post: ${linkedinCharCount} chars`);
|
// No separator — treat entire output as article
|
||||||
|
viralArticle = viralOutput;
|
||||||
|
console.log(` Viral Signal: article ${viralArticle.split(/\s+/).length} words (no LinkedIn section)`);
|
||||||
|
}
|
||||||
|
// Validate viral output isn't too short (LLM may have over-cut)
|
||||||
|
if (viralArticle.split(/\s+/).length < 400) {
|
||||||
|
console.log(" ⚠ Viral Signal output too short — falling back to APM output");
|
||||||
|
viralArticle = stepAPM.text;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
console.log(" LinkedIn post generation skipped");
|
console.log(" Viral Signal skipped (error) — using APM output");
|
||||||
}
|
}
|
||||||
stepsCompleted = 17;
|
stepsCompleted = 17;
|
||||||
|
|
||||||
// Extract only the article from APM output (APM returns clean article only)
|
// ═══ STEP 18: LinkedIn Post ═══
|
||||||
// Fall back to step9.text if APM output looks too short or empty
|
// Use Viral Signal LinkedIn if available, otherwise generate via STEP_LINKEDIN_POST
|
||||||
let finalArticleText = stepAPM.text.trim().length > 200 ? stepAPM.text : step9.text;
|
console.log(" Step 18/18: LinkedIn Post (max 2,800 chars)...");
|
||||||
|
setProgress(draftId, 18, "Step 18/18: LinkedIn Post");
|
||||||
|
let linkedinPost: string | null = viralLinkedinPost;
|
||||||
|
let linkedinCharCount: number | null = viralLinkedinPost ? viralLinkedinPost.length : null;
|
||||||
|
if (!linkedinPost) {
|
||||||
|
// Fallback: dedicated LinkedIn post generator
|
||||||
|
try {
|
||||||
|
const stepLinkedIn = await generate(systemPrompt,
|
||||||
|
STEP_LINKEDIN_POST.replace("{{ARTICLE}}", viralArticle),
|
||||||
|
{ temperature: 0.6, maxTokens: 1024, timeoutMs: 120000 }
|
||||||
|
);
|
||||||
|
linkedinPost = stepLinkedIn.text.trim();
|
||||||
|
linkedinCharCount = linkedinPost.length;
|
||||||
|
} catch {
|
||||||
|
console.log(" LinkedIn post generation skipped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Enforce hard limit — truncate at last sentence before 2800 if too long
|
||||||
|
if (linkedinPost && linkedinPost.length > 2800) {
|
||||||
|
linkedinPost = linkedinPost.slice(0, 2800).replace(/[^.!?]*$/, "").trim();
|
||||||
|
linkedinCharCount = linkedinPost.length;
|
||||||
|
console.log(` LinkedIn post truncated to ${linkedinCharCount} chars`);
|
||||||
|
} else if (linkedinPost) {
|
||||||
|
console.log(` LinkedIn post: ${linkedinCharCount} chars`);
|
||||||
|
}
|
||||||
|
stepsCompleted = 18;
|
||||||
|
|
||||||
|
// Extract article from Viral Signal output (or APM fallback)
|
||||||
|
// Fall back to step9.text if output looks too short or empty
|
||||||
|
let finalArticleText = viralArticle.trim().length > 200 ? viralArticle : step9.text;
|
||||||
const articleMarkers = [
|
const articleMarkers = [
|
||||||
"### COMPLETE FIXED ARTICLE",
|
"### COMPLETE FIXED ARTICLE",
|
||||||
"## COMPLETE FIXED ARTICLE",
|
"## COMPLETE FIXED ARTICLE",
|
||||||
@ -1345,8 +1410,8 @@ async function runLlmPipeline(
|
|||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE blog_drafts
|
`UPDATE blog_drafts
|
||||||
SET draft_content = $1, word_count = $2,
|
SET draft_content = $1, word_count = $2,
|
||||||
generated_by = 'fo-blog-engine-v6',
|
generated_by = 'fo-blog-engine-v7',
|
||||||
pipeline_version = 'v6-precision-mode',
|
pipeline_version = 'v7-viral-signal',
|
||||||
pipeline_steps_completed = $3,
|
pipeline_steps_completed = $3,
|
||||||
auto_qa_score = $4,
|
auto_qa_score = $4,
|
||||||
outline = $5,
|
outline = $5,
|
||||||
@ -1390,13 +1455,13 @@ async function runLlmPipeline(
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearProgress(draftId);
|
clearProgress(draftId);
|
||||||
console.log(`Blog FO Pipeline: ${draftId} complete — ${wordCount} words, ${stepsCompleted}/17 steps, QA: ${(autoQaScore as any)?.overall || "N/A"}/10, LinkedIn: ${linkedinCharCount ?? "n/a"} chars`);
|
console.log(`Blog FO Pipeline: ${draftId} complete — ${wordCount} words, ${stepsCompleted}/18 steps, QA: ${(autoQaScore as any)?.overall || "N/A"}/10, LinkedIn: ${linkedinCharCount ?? "n/a"} chars`);
|
||||||
} catch (llmErr) {
|
} catch (llmErr) {
|
||||||
clearProgress(draftId);
|
clearProgress(draftId);
|
||||||
console.warn(`Blog FO Pipeline failed at step ${stepsCompleted + 1}/16 for ${draftId}: ${(llmErr as Error).message}`);
|
console.warn(`Blog FO Pipeline failed at step ${stepsCompleted + 1}/18 for ${draftId}: ${(llmErr as Error).message}`);
|
||||||
// Update with partial progress
|
// Update with partial progress
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE blog_drafts SET pipeline_steps_completed = $1, pipeline_version = 'v5-narrative-control',
|
`UPDATE blog_drafts SET pipeline_steps_completed = $1, pipeline_version = 'v7-viral-signal',
|
||||||
outline = $2, updated_at = NOW() WHERE id = $3::uuid`,
|
outline = $2, updated_at = NOW() WHERE id = $3::uuid`,
|
||||||
[stepsCompleted, JSON.stringify({ error: (llmErr as Error).message, steps_completed: stepsCompleted }), draftId]
|
[stepsCompleted, JSON.stringify({ error: (llmErr as Error).message, steps_completed: stepsCompleted }), draftId]
|
||||||
).catch(() => {});
|
).catch(() => {});
|
||||||
@ -1562,7 +1627,7 @@ blogRouter.post("/llm/reset-queue", (_req: Request, res: Response) => {
|
|||||||
blogRouter.get("/:id/progress", (req: Request, res: Response) => {
|
blogRouter.get("/:id/progress", (req: Request, res: Response) => {
|
||||||
const p = pipelineProgress.get(String(req.params.id));
|
const p = pipelineProgress.get(String(req.params.id));
|
||||||
if (!p) {
|
if (!p) {
|
||||||
res.json({ success: true, running: false, step: 0, total: 10, label: "Idle", pct: 0 });
|
res.json({ success: true, running: false, step: 0, total: 18, label: "Idle", pct: 0 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.json({ success: true, running: true, ...p });
|
res.json({ success: true, running: true, ...p });
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user