fix: include linkedin_post in GET /api/blog response for SLL matching
This commit is contained in:
parent
d9f5fc253f
commit
15f3ff5bef
@ -16,8 +16,8 @@ import { pool } from "../db/client";
|
||||
const pipelineProgress = new Map<string, { step: number; total: number; label: string; pct: number }>();
|
||||
|
||||
function setProgress(draftId: string, step: number, label: string): void {
|
||||
const pct = Math.round((step / 14) * 92) + 2; // 2%..94% during run, 100% on complete
|
||||
pipelineProgress.set(draftId, { step, total: 14, label, pct });
|
||||
const pct = Math.round((step / 17) * 92) + 2; // 2%..94% during run, 100% on complete
|
||||
pipelineProgress.set(draftId, { step, total: 17, label, pct });
|
||||
}
|
||||
|
||||
function clearProgress(draftId: string): void {
|
||||
@ -1001,20 +1001,24 @@ async function runLlmPipeline(
|
||||
STEP5_REALITY_INJECTION,
|
||||
STEP6_TECHNICAL_DEEPENING,
|
||||
STEP7_OPINION_LAYER,
|
||||
STEP_AFE,
|
||||
STEP8_KILL_AI_TONE,
|
||||
STEP8b_REDUCTION,
|
||||
STEP_AEM,
|
||||
STEP8c_STYLE_LOCK,
|
||||
STEP9_QA_CHECK,
|
||||
STEP10_QUALITY_SCORE,
|
||||
STEP_APM,
|
||||
STEP_LINKEDIN_POST,
|
||||
BLOG_TYPES,
|
||||
buildFeedbackContext,
|
||||
buildSLLContext,
|
||||
withCalibration,
|
||||
} = await import("../llm/fo-blog-pipeline");
|
||||
|
||||
const LLM_OPTS = { temperature: 0.7, maxTokens: 8192, timeoutMs: 480000 };
|
||||
const LLM_REFINE = { temperature: 0.4, maxTokens: 6144, timeoutMs: 480000 };
|
||||
const TOTAL_STEPS = 14; // 10 original + 4b Narrative Control + 8b Reduction + 8c Style Lock + LinkedIn
|
||||
const TOTAL_STEPS = 17; // 16-step pipeline + APM final cut
|
||||
let stepsCompleted = 0;
|
||||
|
||||
try {
|
||||
@ -1034,7 +1038,14 @@ async function runLlmPipeline(
|
||||
})));
|
||||
} catch { /* no feedback yet, that's fine */ }
|
||||
|
||||
const systemPrompt = withCalibration(FO_BLOG_SYSTEM_PROMPT + feedbackContext);
|
||||
// Load SLL learned patterns (safe-fails if no data yet)
|
||||
let sllContext = "";
|
||||
try {
|
||||
sllContext = await buildSLLContext();
|
||||
if (sllContext) console.log(" SLL: Learned patterns injected into system prompt");
|
||||
} catch { /* no SLL data yet, fine */ }
|
||||
|
||||
const systemPrompt = withCalibration(FO_BLOG_SYSTEM_PROMPT + feedbackContext + sllContext);
|
||||
|
||||
// Warmup
|
||||
await generate("Test", "OK", { temperature: 0.1, maxTokens: 8, timeoutMs: 60000 }).catch(() => {});
|
||||
@ -1147,66 +1158,88 @@ async function runLlmPipeline(
|
||||
stepsCompleted = 6;
|
||||
|
||||
// ═══ STEP 6: Technical Deepening ═══
|
||||
console.log(" Step 7/13: Technical Deepening...");
|
||||
setProgress(draftId, 7, "Step 7/13: Technical Deepening");
|
||||
console.log(" Step 7/16: Technical Deepening...");
|
||||
setProgress(draftId, 7, "Step 7/16: Technical Deepening");
|
||||
const step6 = await generate(systemPrompt,
|
||||
STEP6_TECHNICAL_DEEPENING.replace("{{ARTICLE}}", step5.text),
|
||||
LLM_REFINE
|
||||
);
|
||||
stepsCompleted = 6;
|
||||
stepsCompleted = 7;
|
||||
|
||||
// ═══ STEP 7: Opinion Layer ═══
|
||||
console.log(" Step 8/13: Opinion Layer...");
|
||||
setProgress(draftId, 8, "Step 8/13: Opinion Layer");
|
||||
console.log(" Step 8/16: Opinion Layer...");
|
||||
setProgress(draftId, 8, "Step 8/16: Opinion Layer");
|
||||
const step7 = await generate(systemPrompt,
|
||||
STEP7_OPINION_LAYER.replace("{{ARTICLE}}", step6.text),
|
||||
LLM_REFINE
|
||||
);
|
||||
stepsCompleted = 8;
|
||||
|
||||
// ═══ STEP 8: Kill AI Tone ═══
|
||||
console.log(" Step 9/13: Kill AI Tone...");
|
||||
setProgress(draftId, 9, "Step 9/13: Kill AI Tone");
|
||||
const step8 = await generate(systemPrompt,
|
||||
STEP8_KILL_AI_TONE.replace("{{ARTICLE}}", step7.text),
|
||||
// ═══ STEP AFE: Auto-Focus Enforcer (ONE idea, ONE scenario, kill drift) ═══
|
||||
console.log(" Step 9/16: Auto-Focus Enforcer (kill multi-topic drift)...");
|
||||
setProgress(draftId, 9, "Step 9/16: Auto-Focus Enforcer");
|
||||
const stepAFE = await generate(systemPrompt,
|
||||
STEP_AFE.replace("{{ARTICLE}}", step7.text),
|
||||
LLM_REFINE
|
||||
);
|
||||
stepsCompleted = 9;
|
||||
const wordsAFE = stepAFE.text.split(/\s+/).length;
|
||||
const wordsBeforeAFE = step7.text.split(/\s+/).length;
|
||||
const pctAFE = Math.round((1 - wordsAFE / wordsBeforeAFE) * 100);
|
||||
if (pctAFE > 5) console.log(` AFE cut: ${wordsBeforeAFE} → ${wordsAFE} words (−${pctAFE}%) — drift removed`);
|
||||
|
||||
// ═══ STEP 8: Kill AI Tone ═══
|
||||
console.log(" Step 10/16: Kill AI Tone...");
|
||||
setProgress(draftId, 10, "Step 10/16: Kill AI Tone");
|
||||
const step8 = await generate(systemPrompt,
|
||||
STEP8_KILL_AI_TONE.replace("{{ARTICLE}}", stepAFE.text),
|
||||
LLM_REFINE
|
||||
);
|
||||
stepsCompleted = 10;
|
||||
|
||||
// ═══ STEP 8b: Reduction Engine (5-pass: Repetition Kill → Tech Prune → Flow Rebuild → Weight Correction → Humanization) ═══
|
||||
console.log(" Step 10/14: Reduction Engine (5-pass, target 700-1000 words)...");
|
||||
setProgress(draftId, 10, "Step 10/14: Reduction Engine");
|
||||
console.log(" Step 11/16: Reduction Engine (5-pass, target 700-1000 words)...");
|
||||
setProgress(draftId, 11, "Step 11/16: Reduction Engine");
|
||||
const step8b = await generate(systemPrompt,
|
||||
STEP8b_REDUCTION.replace("{{ARTICLE}}", step8.text),
|
||||
LLM_REFINE
|
||||
);
|
||||
stepsCompleted = 10;
|
||||
stepsCompleted = 11;
|
||||
const wordsAfter = step8b.text.split(/\s+/).length;
|
||||
const wordsBefore = step8.text.split(/\s+/).length;
|
||||
const pctChange = Math.round((1 - wordsAfter / wordsBefore) * 100);
|
||||
console.log(` After reduction: ${wordsAfter} words (was ${wordsBefore}, −${pctChange}%) ${wordsAfter > 1300 ? "⚠ WARNING: >1300 words" : wordsAfter < 600 ? "⚠ WARNING: <600 words" : "✓ in target range"}`);
|
||||
|
||||
// ═══ STEP 8c: Style Lock ═══
|
||||
console.log(" Step 11/14: Style Lock (tone consistency + scope/SKU fixes)...");
|
||||
setProgress(draftId, 11, "Step 11/14: Style Lock");
|
||||
const step8c = await generate(systemPrompt,
|
||||
STEP8c_STYLE_LOCK.replace("{{ARTICLE}}", step8b.text),
|
||||
LLM_REFINE
|
||||
);
|
||||
stepsCompleted = 11;
|
||||
|
||||
// ═══ STEP 9: QA Check ═══
|
||||
console.log(" Step 12/14: QA Check...");
|
||||
setProgress(draftId, 12, "Step 12/14: QA Check");
|
||||
const step9 = await generate(systemPrompt,
|
||||
STEP9_QA_CHECK.replace("{{ARTICLE}}", step8c.text),
|
||||
// ═══ STEP AEM: Auto-Editor Mode (Senior Engineer voice polish) ═══
|
||||
console.log(" Step 12/16: Auto-Editor Mode (senior engineer voice polish)...");
|
||||
setProgress(draftId, 12, "Step 12/16: Auto-Editor Mode");
|
||||
const stepAEM = await generate(systemPrompt,
|
||||
STEP_AEM.replace("{{ARTICLE}}", step8b.text),
|
||||
LLM_REFINE
|
||||
);
|
||||
stepsCompleted = 12;
|
||||
|
||||
// ═══ STEP 8c: Style Lock ═══
|
||||
console.log(" Step 13/16: Style Lock (tone consistency + scope/SKU fixes)...");
|
||||
setProgress(draftId, 13, "Step 13/16: Style Lock");
|
||||
const step8c = await generate(systemPrompt,
|
||||
STEP8c_STYLE_LOCK.replace("{{ARTICLE}}", stepAEM.text),
|
||||
LLM_REFINE
|
||||
);
|
||||
stepsCompleted = 13;
|
||||
|
||||
// ═══ STEP 9: QA Check ═══
|
||||
console.log(" Step 14/16: QA Check...");
|
||||
setProgress(draftId, 14, "Step 14/16: QA Check");
|
||||
const step9 = await generate(systemPrompt,
|
||||
STEP9_QA_CHECK.replace("{{ARTICLE}}", step8c.text),
|
||||
LLM_REFINE
|
||||
);
|
||||
stepsCompleted = 14;
|
||||
|
||||
// ═══ STEP 10: Quality Score ═══
|
||||
console.log(" Step 13/14: Quality Score...");
|
||||
setProgress(draftId, 13, "Step 13/14: Quality Score");
|
||||
console.log(" Step 15/16: Quality Score...");
|
||||
setProgress(draftId, 15, "Step 15/16: Quality Score");
|
||||
let autoQaScore: Record<string, unknown> | null = null;
|
||||
try {
|
||||
const step10 = await generate(systemPrompt,
|
||||
@ -1222,16 +1255,29 @@ async function runLlmPipeline(
|
||||
} catch {
|
||||
console.log(" Quality scoring skipped (parse error)");
|
||||
}
|
||||
stepsCompleted = 13;
|
||||
stepsCompleted = 15;
|
||||
|
||||
// ═══ 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)...");
|
||||
setProgress(draftId, 16, "Step 16/17: Auto-Precision Mode");
|
||||
const stepAPM = await generate(systemPrompt,
|
||||
STEP_APM.replace("{{ARTICLE}}", step9.text),
|
||||
LLM_REFINE
|
||||
);
|
||||
stepsCompleted = 16;
|
||||
const wordsAPM = stepAPM.text.split(/\s+/).length;
|
||||
const wordsBeforeAPM = step9.text.split(/\s+/).length;
|
||||
const pctAPM = Math.round((1 - wordsAPM / wordsBeforeAPM) * 100);
|
||||
console.log(` APM: ${wordsBeforeAPM} → ${wordsAPM} words (−${pctAPM}%) — precision cut done`);
|
||||
|
||||
// ═══ LinkedIn Post ═══
|
||||
console.log(" Step 14/14: LinkedIn Post (max 2,800 chars)...");
|
||||
setProgress(draftId, 14, "Step 14/14: LinkedIn Post");
|
||||
console.log(" Step 17/17: LinkedIn Post (max 2,800 chars)...");
|
||||
setProgress(draftId, 17, "Step 17/17: LinkedIn Post");
|
||||
let linkedinPost: string | null = null;
|
||||
let linkedinCharCount: number | null = null;
|
||||
try {
|
||||
const stepLinkedIn = await generate(systemPrompt,
|
||||
STEP_LINKEDIN_POST.replace("{{ARTICLE}}", step9.text),
|
||||
STEP_LINKEDIN_POST.replace("{{ARTICLE}}", stepAPM.text),
|
||||
{ temperature: 0.6, maxTokens: 1024, timeoutMs: 120000 }
|
||||
);
|
||||
linkedinPost = stepLinkedIn.text.trim();
|
||||
@ -1247,11 +1293,11 @@ async function runLlmPipeline(
|
||||
} catch {
|
||||
console.log(" LinkedIn post generation skipped");
|
||||
}
|
||||
stepsCompleted = 14;
|
||||
stepsCompleted = 17;
|
||||
|
||||
// Extract only the article from STEP9 output (QA returns review + fixed article)
|
||||
// Look for "COMPLETE FIXED ARTICLE" marker and take everything after it
|
||||
let finalArticleText = step9.text;
|
||||
// Extract only the article from APM output (APM returns clean article only)
|
||||
// Fall back to step9.text if APM output looks too short or empty
|
||||
let finalArticleText = stepAPM.text.trim().length > 200 ? stepAPM.text : step9.text;
|
||||
const articleMarkers = [
|
||||
"### COMPLETE FIXED ARTICLE",
|
||||
"## COMPLETE FIXED ARTICLE",
|
||||
@ -1259,13 +1305,16 @@ async function runLlmPipeline(
|
||||
"---\n\n**You're",
|
||||
"---\n\nYou're",
|
||||
];
|
||||
// Also check step9 for QA markers (APM may have stripped them already)
|
||||
for (const marker of articleMarkers) {
|
||||
const idx = step9.text.indexOf(marker);
|
||||
if (idx !== -1) {
|
||||
// Skip past the marker line itself
|
||||
const afterMarker = step9.text.slice(idx + marker.length).trimStart();
|
||||
// Strip leading --- separator if present
|
||||
finalArticleText = afterMarker.replace(/^---\s*\n/, "").trimStart();
|
||||
const extractedFromQA = afterMarker.replace(/^---\s*\n/, "").trimStart();
|
||||
// Only use QA extraction if it's meaningfully longer than APM output
|
||||
if (extractedFromQA.split(/\s+/).length > finalArticleText.split(/\s+/).length * 0.8) {
|
||||
finalArticleText = extractedFromQA;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -1280,18 +1329,29 @@ async function runLlmPipeline(
|
||||
const wordCount = draftContent.split(/\s+/).length;
|
||||
const finalIssues = validateArticle(draftContent);
|
||||
|
||||
// Update the draft in DB
|
||||
// Hard minimum word count gate (1200 for LLM pipeline)
|
||||
if (wordCount < 1200) {
|
||||
const shortMsg = `⚠ WORD COUNT FAIL: ${wordCount} words — minimum 1200 for LLM pipeline`;
|
||||
console.log(` ${shortMsg}`);
|
||||
if (!finalIssues.includes(`Too short: ${wordCount} words`)) {
|
||||
finalIssues.push(`Too short: ${wordCount} words (minimum 1200 for LLM pipeline — article needs expansion)`);
|
||||
}
|
||||
} else {
|
||||
console.log(` ✓ Word count: ${wordCount} words (≥1200 — OK)`);
|
||||
}
|
||||
|
||||
// Update the draft in DB — promote to 'ready' on full pipeline completion
|
||||
await pool.query(
|
||||
`UPDATE blog_drafts
|
||||
SET draft_content = $1, word_count = $2,
|
||||
generated_by = 'fo-blog-engine-v5',
|
||||
pipeline_version = 'v5-narrative-control',
|
||||
generated_by = 'fo-blog-engine-v6',
|
||||
pipeline_version = 'v6-precision-mode',
|
||||
pipeline_steps_completed = $3,
|
||||
auto_qa_score = $4,
|
||||
outline = $5,
|
||||
linkedin_post = $6,
|
||||
linkedin_char_count = $7,
|
||||
status = 'draft',
|
||||
status = 'review',
|
||||
updated_at = NOW()
|
||||
WHERE id = $8::uuid`,
|
||||
[
|
||||
@ -1329,10 +1389,10 @@ async function runLlmPipeline(
|
||||
}
|
||||
|
||||
clearProgress(draftId);
|
||||
console.log(`Blog FO Pipeline: ${draftId} complete — ${wordCount} words, ${stepsCompleted}/14 steps, QA: ${(autoQaScore as any)?.overall || "N/A"}/10, LinkedIn: ${linkedinCharCount ?? "n/a"} chars`);
|
||||
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`);
|
||||
} catch (llmErr) {
|
||||
clearProgress(draftId);
|
||||
console.warn(`Blog FO Pipeline failed at step ${stepsCompleted + 1}/14 for ${draftId}: ${(llmErr as Error).message}`);
|
||||
console.warn(`Blog FO Pipeline failed at step ${stepsCompleted + 1}/16 for ${draftId}: ${(llmErr as Error).message}`);
|
||||
// Update with partial progress
|
||||
await pool.query(
|
||||
`UPDATE blog_drafts SET pipeline_steps_completed = $1, pipeline_version = 'v5-narrative-control',
|
||||
@ -1382,6 +1442,19 @@ blogRouter.post("/generate", async (req: Request, res: Response) => {
|
||||
|
||||
const data = await gatherBlogData(keywords, selectedTopic);
|
||||
|
||||
// Clean up stale template drafts for the same title (idempotent regeneration)
|
||||
// If a template draft already exists for this title, remove it before creating a fresh one
|
||||
await pool.query(
|
||||
`DELETE FROM blog_feedback WHERE blog_id IN (
|
||||
SELECT id FROM blog_drafts WHERE title = $1 AND generated_by = 'tip-blog-engine-template'
|
||||
)`,
|
||||
[title]
|
||||
).catch(() => {});
|
||||
await pool.query(
|
||||
`DELETE FROM blog_drafts WHERE title = $1 AND generated_by = 'tip-blog-engine-template'`,
|
||||
[title]
|
||||
).catch(() => {});
|
||||
|
||||
// Always create a template draft first (instant response)
|
||||
const draftContent = generateTemplateDraft(title, selectedTopic, data);
|
||||
const wordCount = draftContent.split(/\s+/).length;
|
||||
@ -1459,7 +1532,7 @@ blogRouter.post("/generate", async (req: Request, res: Response) => {
|
||||
blogRouter.get("/", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT id, title, topic, target_audience, status, word_count, seo_keywords, generated_by, created_at
|
||||
`SELECT id, title, topic, target_audience, status, word_count, seo_keywords, generated_by, created_at, linkedin_post
|
||||
FROM blog_drafts
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 50`,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user