feat: blog engine v5 — narrative control + linkedin post + min words fix
- STEP4b_NARRATIVE_CONTROL: new pipeline step after draft; detects wrong
narrative (technology blamed instead of processes), applies anti-FUD filter,
reality reframe ("this becomes a problem when..."), Flexoptix voice check
- System prompt: NARRATIVE CONTROL RULE added as absolute rule #1
- Gold Standard 4: corrected "compatible vs OEM" article added as reference
- Minimum words: STEP4 raised from 1500 to 2500 words (final output was 750)
- Reduction pass: 25-35% → 15-25%, target 1500-2000 words final
- STEP_LINKEDIN_POST: generates LinkedIn post ≤2800 chars (hard limit 3000);
stores in blog_drafts.linkedin_post + linkedin_char_count column
- Pipeline now 14 steps: v5-narrative-control
- Migration 024: linkedin_post + linkedin_char_count columns in blog_drafts
This commit is contained in:
parent
74dcc14e1e
commit
7db9fad108
@ -27,6 +27,32 @@ export const FO_BLOG_SYSTEM_PROMPT = `You are a senior network engineer with 20+
|
||||
|
||||
You write for the FLEXOPTIX technical blog. Your readers are network engineers who build and operate real infrastructure.
|
||||
|
||||
════════════════════════════════════════════════════════
|
||||
NARRATIVE CONTROL — ABSOLUTE RULE #1 (overrides everything)
|
||||
════════════════════════════════════════════════════════
|
||||
|
||||
Flexoptix sells compatible optics. This blog must make engineers trust compatible optics MORE, not less.
|
||||
|
||||
CORE PRINCIPLE: Technology is NEVER the root problem. Bad assumptions and bad processes are.
|
||||
|
||||
✔ CORRECT FRAMING:
|
||||
"compatible optics shift validation responsibility to the operator"
|
||||
"this becomes a problem when validation is skipped"
|
||||
"400G exposes what was always marginal — the optics don't introduce new problems"
|
||||
"the failure isn't the optic. it's everything around it."
|
||||
|
||||
✗ FORBIDDEN FRAMING (hard fail — rewrite immediately):
|
||||
"compatible optics cause problems" → OEM FUD. Never.
|
||||
"this technology is risky" → Only allowed with explicit condition: "risky when X"
|
||||
Stacking 5+ failure scenarios without resolution → fear-mongering, not engineering
|
||||
Implying OEM = safe, compatible = gamble → not Flexoptix voice
|
||||
|
||||
FINAL OUTCOME TEST:
|
||||
If a reader finishes this article thinking "I'll just buy OEM to be safe" → article FAILED.
|
||||
If a reader finishes thinking "I need to validate properly before deployment" → article PASSED.
|
||||
|
||||
════════════════════════════════════════════════════════
|
||||
|
||||
YOUR MINDSET:
|
||||
- You write like an engineer at 2:17 AM in the DC, not like a marketing department
|
||||
- You base everything on real problems, not spec sheets
|
||||
@ -356,7 +382,10 @@ CONTENT APPROACH:
|
||||
- Every number gets context (deployment size, vendor type, conditions)
|
||||
- Max 3-4 core ideas — pick the best and develop them through experience
|
||||
|
||||
MINIMUM 1500 words. No placeholders. No TODO markers. No sections. Complete prose article.
|
||||
MINIMUM 2500 words. No placeholders. No TODO markers. No sections. Complete prose article.
|
||||
|
||||
NARRATIVE REMINDER: The failure is never the optic. It's the environment, the assumptions, the process.
|
||||
Write with that framing from the first sentence.
|
||||
|
||||
Context data from Flexoptix database (verified — use exactly as provided):
|
||||
{{CONTEXT_DATA}}
|
||||
@ -364,6 +393,91 @@ Context data from Flexoptix database (verified — use exactly as provided):
|
||||
Outline:
|
||||
{{OUTLINE}}`;
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// STEP 4b: NARRATIVE CONTROL
|
||||
// (2026-04-04: Added — LLM systematic error: correct problem detection,
|
||||
// wrong cause assignment. Blames technology instead of processes.)
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
export const STEP4b_NARRATIVE_CONTROL = `FLEXOPTIX NARRATIVE CONTROL — read this article line by line and apply four corrections.
|
||||
|
||||
════════════════════════════════════════════════════════
|
||||
CORRECTION 1: ROOT CAUSE ASSIGNMENT
|
||||
════════════════════════════════════════════════════════
|
||||
|
||||
The article may frame technology (optics, standards, form factors) as the root problem.
|
||||
That is wrong. Technology is never the root problem. Fix it everywhere it appears.
|
||||
|
||||
FIND patterns like:
|
||||
- "compatible optics cause [X]"
|
||||
- "this technology introduces [risk/problems/failure]"
|
||||
- "[technology] can lead to [negative outcome]" (without specifying the condition)
|
||||
|
||||
REPLACE with correct framing:
|
||||
- "unvalidated deployments of [technology] cause [X]"
|
||||
- "[technology] exposes problems that were always present in the environment"
|
||||
- "this becomes a problem when [specific condition — dirty connectors / firmware mismatch / MMF plant reused for SMF]"
|
||||
|
||||
The optic is not the problem. The optic is the test that reveals existing problems.
|
||||
|
||||
════════════════════════════════════════════════════════
|
||||
CORRECTION 2: ANTI-FUD FILTER
|
||||
════════════════════════════════════════════════════════
|
||||
|
||||
Scan for fear-based statements. Rewrite any of these patterns:
|
||||
- Exaggerated risk without condition: "can be brutal" → "is expensive when X"
|
||||
- Stacked failure scenarios (4+ worst-cases in a row, no resolution) → keep max 2-3, add one positive conditional
|
||||
- OEM-style framing: "compatible = risky" or "cheap = unreliable" → rewrite as "compatible = operator-owned validation"
|
||||
- Absolute negative claims without context: "X always fails" → "X fails when Y"
|
||||
|
||||
Every problem statement must answer implicitly: under what condition? What makes it worse? What fixes it?
|
||||
|
||||
════════════════════════════════════════════════════════
|
||||
CORRECTION 3: REALITY REFRAME
|
||||
════════════════════════════════════════════════════════
|
||||
|
||||
For each failure described in the article, ensure it is framed as conditional:
|
||||
|
||||
"this becomes a problem when…"
|
||||
|
||||
If the article says: "compatible optics have interoperability issues"
|
||||
Rewrite as: "interoperability issues surface when firmware combinations are untested, cabling is marginal, or validation was skipped — the same conditions that cause OEM issues"
|
||||
|
||||
If the article says: "400G DR4 has tight loss budgets"
|
||||
Rewrite as: "400G DR4 has tighter loss budgets than 100G SR4, which means anything that was marginal before becomes visible now"
|
||||
|
||||
The reframe doesn't soften the problem. It places it correctly: environment and process, not hardware.
|
||||
|
||||
════════════════════════════════════════════════════════
|
||||
CORRECTION 4: FLEXOPTIX VOICE CHECK
|
||||
════════════════════════════════════════════════════════
|
||||
|
||||
Apply the Flexoptix outcome test:
|
||||
|
||||
Read the final paragraph. Ask: what does a reader conclude?
|
||||
|
||||
WRONG conclusion: "I should just buy OEM to be safe."
|
||||
→ Rewrite the conclusion or ending to shift this.
|
||||
|
||||
RIGHT conclusion: "I need to validate properly. Compatible optics work when I do my part."
|
||||
→ This is the Flexoptix message.
|
||||
|
||||
The blog is not an OEM sales tool. It's proof that engineers who know what they're doing use compatible optics successfully.
|
||||
|
||||
════════════════════════════════════════════════════════
|
||||
OUTPUT RULE
|
||||
════════════════════════════════════════════════════════
|
||||
|
||||
Return ONLY the corrected article.
|
||||
No explanation of what you changed.
|
||||
No commentary, no "I rewrote X".
|
||||
Start directly with the article hook. End with the final sentence.
|
||||
Do not add new sections or headers.
|
||||
Do not change the overall structure — only fix narrative framing where needed.
|
||||
|
||||
Article:
|
||||
{{ARTICLE}}`;
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// STEP 5: REALITY INJECTION
|
||||
// ═══════════════════════════════════════════════════════
|
||||
@ -1011,6 +1125,102 @@ KEY ELEMENTS OF THIS STYLE B EXAMPLE 3:
|
||||
- Ending: the cost is lost time, stated simply and directly
|
||||
- ZERO section headers, ZERO bullet lists, ZERO numbered steps
|
||||
|
||||
━━━ STYLE B GOLD EXAMPLE 4 (2026-04-04 validated — Compatible vs OEM, Narrative Control) ━━━
|
||||
Topic: Price War / compatible optics. CRITICAL: This is the corrected narrative — compatible ≠ problem.
|
||||
This example was generated after wrong narrative feedback. Use as reference for ANY compatible optics article.
|
||||
|
||||
"You're looking at a quote for a few hundred 400G optics.
|
||||
|
||||
OEM pricing is what it always is. Then you look at compatible optics, and suddenly the numbers drop hard. Same form factor, same standards, much lower price.
|
||||
|
||||
That's usually the moment where people get uncomfortable.
|
||||
|
||||
Because the first instinct isn't excitement. It's suspicion.
|
||||
|
||||
'What's the catch?'
|
||||
|
||||
There usually isn't one.
|
||||
|
||||
At least not in the way people think.
|
||||
|
||||
The optics themselves aren't the problem. Modern compatible modules are solid. Interop works. Standards are real. If something doesn't come up, it's rarely because the optic is 'cheap'.
|
||||
|
||||
But that doesn't mean deployments are frictionless.
|
||||
|
||||
Because what actually breaks isn't the optic.
|
||||
It's everything around it.
|
||||
|
||||
Most issues people run into with compatible optics look like this:
|
||||
|
||||
A link comes up, but behaves differently under load.
|
||||
Another one shows CRC errors that shouldn't be there.
|
||||
Everything works in the lab, but production feels inconsistent.
|
||||
|
||||
The natural reaction is to blame the optics.
|
||||
|
||||
That's almost always the wrong conclusion.
|
||||
|
||||
What's actually happening is much less exciting. You've changed one variable — the optic — and suddenly everything else in your setup gets exposed.
|
||||
|
||||
Cabling that was marginal but 'good enough' before now sits right at the edge.
|
||||
Connectors that were never properly cleaned start to matter.
|
||||
Firmware combinations you never tested together suddenly behave differently.
|
||||
|
||||
None of that shows up in a clean lab test. It shows up when you deploy at scale.
|
||||
|
||||
I've seen this play out more than once. Everything validated, everything looking good. Then production rollout starts and a handful of links behave strangely. Not down, just unstable enough to be annoying.
|
||||
|
||||
So you swap optics. No change. You swap ports. No change.
|
||||
|
||||
Eventually someone cleans the connectors properly — not visually, actually checks them — and the problem disappears.
|
||||
|
||||
Same optics. Same config. Different result.
|
||||
|
||||
That's the moment where the narrative usually flips.
|
||||
|
||||
Polarity is another classic. It's one of those things that's assumed to be correct because it always has been. Until it isn't.
|
||||
|
||||
At 400G, a mismatch in your MPO layout doesn't give you degraded performance. It gives you a dead link that looks completely fine from a config perspective.
|
||||
|
||||
So again, optics get blamed first. Physical layer gets checked last.
|
||||
|
||||
And then there's the real cost nobody talks about. Not optics. Time.
|
||||
|
||||
The time spent debugging issues that don't have a single clean root cause. The time spent validating combinations that were never tested together. The time lost because assumptions from previous generations don't hold anymore.
|
||||
|
||||
Compatible optics don't remove that complexity.
|
||||
|
||||
But they don't introduce it either.
|
||||
|
||||
They just remove the price premium.
|
||||
|
||||
If you treat a deployment the same way you did five years ago — minimal validation, assumptions about cabling, trusting that everything 'just works' — you will run into issues. It doesn't matter if you use OEM or compatible.
|
||||
|
||||
The difference is: with compatible optics, people are quicker to blame the hardware.
|
||||
|
||||
What actually works is pretty simple. Validate the setup you're going to run, not a simplified version of it. Treat the physical layer seriously — cleaning, inspection, polarity, mapping. Test combinations of optics, platforms, and firmware before scaling.
|
||||
|
||||
Because in most cases, the optics are doing exactly what they're supposed to.
|
||||
|
||||
They're just showing you everything else that isn't."
|
||||
|
||||
KEY ELEMENTS OF THIS STYLE B EXAMPLE 4:
|
||||
- Compatible optics NOT framed as problem — framed as "exposing existing problems"
|
||||
- Root causes named correctly: cabling, connectors, firmware combinations, assumptions
|
||||
- No fear-mongering — calm, factual, slightly uncomfortable
|
||||
- Ending shifts responsibility to process, not to product choice
|
||||
- Zero section headers, zero bullet lists
|
||||
- Reader conclusion: "I need better validation" — not "I need OEM"
|
||||
|
||||
━━━ WHAT TRIGGERED THIS GOLD STANDARD (learn from the failure) ━━━
|
||||
WRONG version wrote: "compatible optics = hidden costs, extra QA, complex validation"
|
||||
→ This is OEM FUD. It implies compatible = risky by default.
|
||||
→ Flexoptix sells compatible optics. This narrative destroys the brand.
|
||||
|
||||
CORRECT version writes: "validation gaps = hidden costs, not optic choice"
|
||||
→ Any deployment — OEM or compatible — fails when validation is skipped.
|
||||
→ Compatible optics just make engineers more likely to blame the hardware instead of their process.
|
||||
|
||||
WRONG PATTERNS (both styles — never produce):
|
||||
❌ "Thoroughly Test Your PoE Budget:" (PoE = wrong context, checklist = wrong format)
|
||||
❌ "QSFP-DD DR4 (Direct Attach)" (DR4 ≠ Direct Attach — DAC is Direct Attach Copper)
|
||||
@ -1066,15 +1276,78 @@ POWER / LOSS BUDGET PRECISION (always apply):
|
||||
`;
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// STEP 8b: REDUCTION PASS — Remove 25-35% of content
|
||||
// STEP LINKEDIN: Generate LinkedIn post from final article
|
||||
// (2026-04-04: LinkedIn hard limit = 3,000 chars. Optimal = 800-1500 chars.)
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
export const STEP_LINKEDIN_POST = `Write a LinkedIn post for this article.
|
||||
|
||||
HARD LIMIT: Maximum 2,800 characters total (LinkedIn's limit is 3,000 — stay well under it).
|
||||
OPTIMAL LENGTH: 800–1,400 characters. This gets read in full, without "see more" truncation.
|
||||
|
||||
STRUCTURE (in this order):
|
||||
1. HOOK LINE (first 1-2 sentences — this is the ONLY part visible before "see more")
|
||||
- Start with a bold statement, uncomfortable truth, or specific number
|
||||
- NOT "I just published a new blog post"
|
||||
- NOT "Check out my latest article"
|
||||
- Something that makes an engineer stop scrolling
|
||||
|
||||
2. BODY (3-5 short paragraphs, 1-2 sentences each)
|
||||
- One insight per paragraph
|
||||
- Leave space between paragraphs (line breaks)
|
||||
- Keep it specific — numbers, conditions, real scenarios
|
||||
- Write for engineers, not recruiters
|
||||
|
||||
3. CALL TO ACTION (1 sentence)
|
||||
- Direct link invite: "Full breakdown on the Flexoptix blog — link in first comment."
|
||||
- Do NOT include a URL in the post itself (kills reach on LinkedIn)
|
||||
|
||||
4. HASHTAGS (last line, 3-5 tags)
|
||||
- Use: #OpticalNetworking #DataCenter #400G #Flexoptix + one topic-specific tag
|
||||
- Never more than 5 hashtags
|
||||
|
||||
STYLE RULES:
|
||||
- Engineer voice, not LinkedIn influencer voice
|
||||
- No emojis unless one very strategic one at the start of the hook
|
||||
- No "I'm thrilled to announce" / "Excited to share"
|
||||
- No bullet lists inside the post — use short paragraphs instead
|
||||
- No markdown, no bold (**text**), no headers
|
||||
- Write as if you're the engineer who wrote the article, not a social media manager
|
||||
|
||||
EXAMPLE STRUCTURE (700 chars, correct format):
|
||||
|
||||
At 400G, your existing cabling becomes your biggest risk. Not the optics.
|
||||
|
||||
When teams move from 100G SR4 to 400G DR4, they assume the cabling stays the same. Both use 8 fibers. Looks identical on paper.
|
||||
|
||||
In production, everything tightens up. Loss budgets shrink. Margins disappear. Connectors that were 'fine' for years suddenly cause CRC errors.
|
||||
|
||||
The optics don't fail. They expose what was always marginal.
|
||||
|
||||
We put together a full breakdown of what actually breaks — and what to look for before deployment. Link in first comment.
|
||||
|
||||
#OpticalNetworking #400G #DataCenter #NetworkEngineering #Flexoptix
|
||||
|
||||
---
|
||||
|
||||
COUNT YOUR OUTPUT: After writing, count the total characters (including spaces and hashtags). If over 2,800 — cut.
|
||||
|
||||
Return ONLY the LinkedIn post text. Nothing else. No explanation. No "Here is the post:".
|
||||
|
||||
Article:
|
||||
{{ARTICLE}}`;
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// STEP 8b: REDUCTION PASS — Remove 15-25% of content
|
||||
// (2026-04-04: Added based on field feedback — articles were too long,
|
||||
// repeated concepts, and "assembled" rather than written)
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
export const STEP8b_REDUCTION = `Cut this article by 25–35%.
|
||||
export const STEP8b_REDUCTION = `Cut this article by 15–25%.
|
||||
|
||||
This is not optional. After the previous passes, the article has grown too long and repeats itself.
|
||||
This is not optional. After the previous passes, the article has grown and accumulated some padding.
|
||||
The goal is a tighter, more natural text — not a shorter version of the same article.
|
||||
Target: the final article should be 1500–2000 words. Do not cut below 1500 words.
|
||||
|
||||
WHAT TO REMOVE:
|
||||
- Any concept explained more than once (pick its best version, cut the rest)
|
||||
|
||||
@ -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 / 12) * 92) + 2; // 2%..94% during run, 100% on complete
|
||||
pipelineProgress.set(draftId, { step, total: 12, label, pct });
|
||||
const pct = Math.round((step / 14) * 92) + 2; // 2%..94% during run, 100% on complete
|
||||
pipelineProgress.set(draftId, { step, total: 14, label, pct });
|
||||
}
|
||||
|
||||
function clearProgress(draftId: string): void {
|
||||
@ -997,6 +997,7 @@ async function runLlmPipeline(
|
||||
STEP2_ANGLE_SELECTION,
|
||||
STEP3_OUTLINE,
|
||||
STEP4_MASTER_DRAFT,
|
||||
STEP4b_NARRATIVE_CONTROL,
|
||||
STEP5_REALITY_INJECTION,
|
||||
STEP6_TECHNICAL_DEEPENING,
|
||||
STEP7_OPINION_LAYER,
|
||||
@ -1005,14 +1006,15 @@ async function runLlmPipeline(
|
||||
STEP8c_STYLE_LOCK,
|
||||
STEP9_QA_CHECK,
|
||||
STEP10_QUALITY_SCORE,
|
||||
STEP_LINKEDIN_POST,
|
||||
BLOG_TYPES,
|
||||
buildFeedbackContext,
|
||||
withCalibration,
|
||||
} = await import("../llm/fo-blog-pipeline");
|
||||
|
||||
const LLM_OPTS = { temperature: 0.7, maxTokens: 6144, timeoutMs: 480000 };
|
||||
const LLM_OPTS = { temperature: 0.7, maxTokens: 8192, timeoutMs: 480000 };
|
||||
const LLM_REFINE = { temperature: 0.4, maxTokens: 6144, timeoutMs: 480000 };
|
||||
const TOTAL_STEPS = 12; // 10 original + 8b Reduction + 8c Style Lock
|
||||
const TOTAL_STEPS = 14; // 10 original + 4b Narrative Control + 8b Reduction + 8c Style Lock + LinkedIn
|
||||
let stepsCompleted = 0;
|
||||
|
||||
try {
|
||||
@ -1125,18 +1127,28 @@ async function runLlmPipeline(
|
||||
stepsCompleted = 4;
|
||||
console.log(` Draft: ${step4.text.split(/\s+/).length} words`);
|
||||
|
||||
// ═══ STEP 5: Reality Injection ═══
|
||||
console.log(" Step 5/10: Reality Injection...");
|
||||
setProgress(draftId, 5, "Step 5/10: Reality Injection");
|
||||
const step5 = await generate(systemPrompt,
|
||||
STEP5_REALITY_INJECTION.replace("{{DRAFT}}", step4.text),
|
||||
// ═══ STEP 4b: Narrative Control ═══
|
||||
console.log(" Step 5/13: Narrative Control (framing check + anti-FUD)...");
|
||||
setProgress(draftId, 5, "Step 5/13: Narrative Control");
|
||||
const step4b = await generate(systemPrompt,
|
||||
STEP4b_NARRATIVE_CONTROL.replace("{{ARTICLE}}", step4.text),
|
||||
LLM_REFINE
|
||||
);
|
||||
stepsCompleted = 5;
|
||||
console.log(` After narrative control: ${step4b.text.split(/\s+/).length} words`);
|
||||
|
||||
// ═══ STEP 5: Reality Injection ═══
|
||||
console.log(" Step 6/13: Reality Injection...");
|
||||
setProgress(draftId, 6, "Step 6/13: Reality Injection");
|
||||
const step5 = await generate(systemPrompt,
|
||||
STEP5_REALITY_INJECTION.replace("{{DRAFT}}", step4b.text),
|
||||
LLM_REFINE
|
||||
);
|
||||
stepsCompleted = 6;
|
||||
|
||||
// ═══ STEP 6: Technical Deepening ═══
|
||||
console.log(" Step 6/10: Technical Deepening...");
|
||||
setProgress(draftId, 6, "Step 6/10: Technical Deepening");
|
||||
console.log(" Step 7/13: Technical Deepening...");
|
||||
setProgress(draftId, 7, "Step 7/13: Technical Deepening");
|
||||
const step6 = await generate(systemPrompt,
|
||||
STEP6_TECHNICAL_DEEPENING.replace("{{ARTICLE}}", step5.text),
|
||||
LLM_REFINE
|
||||
@ -1144,54 +1156,54 @@ async function runLlmPipeline(
|
||||
stepsCompleted = 6;
|
||||
|
||||
// ═══ STEP 7: Opinion Layer ═══
|
||||
console.log(" Step 7/10: Opinion Layer...");
|
||||
setProgress(draftId, 7, "Step 7/10: Opinion Layer");
|
||||
console.log(" Step 8/13: Opinion Layer...");
|
||||
setProgress(draftId, 8, "Step 8/13: Opinion Layer");
|
||||
const step7 = await generate(systemPrompt,
|
||||
STEP7_OPINION_LAYER.replace("{{ARTICLE}}", step6.text),
|
||||
LLM_REFINE
|
||||
);
|
||||
stepsCompleted = 7;
|
||||
stepsCompleted = 8;
|
||||
|
||||
// ═══ STEP 8: Kill AI Tone ═══
|
||||
console.log(" Step 8/10: Kill AI Tone...");
|
||||
setProgress(draftId, 8, "Step 8/10: 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),
|
||||
LLM_REFINE
|
||||
);
|
||||
stepsCompleted = 8;
|
||||
stepsCompleted = 9;
|
||||
|
||||
// ═══ STEP 8b: Reduction Pass ═══
|
||||
console.log(" Step 9/12: Reduction Pass (remove 25-35%)...");
|
||||
setProgress(draftId, 9, "Step 9/12: Reduction Pass");
|
||||
console.log(" Step 10/13: Reduction Pass (remove 15-25%, keep ≥1500 words)...");
|
||||
setProgress(draftId, 10, "Step 10/13: Reduction Pass");
|
||||
const step8b = await generate(systemPrompt,
|
||||
STEP8b_REDUCTION.replace("{{ARTICLE}}", step8.text),
|
||||
LLM_REFINE
|
||||
);
|
||||
stepsCompleted = 9;
|
||||
stepsCompleted = 10;
|
||||
console.log(` After reduction: ${step8b.text.split(/\s+/).length} words (was ${step8.text.split(/\s+/).length})`);
|
||||
|
||||
// ═══ STEP 8c: Style Lock ═══
|
||||
console.log(" Step 10/12: Style Lock (tone consistency + scope/SKU fixes)...");
|
||||
setProgress(draftId, 10, "Step 10/12: Style Lock");
|
||||
console.log(" Step 11/13: Style Lock (tone consistency + scope/SKU fixes)...");
|
||||
setProgress(draftId, 11, "Step 11/13: Style Lock");
|
||||
const step8c = await generate(systemPrompt,
|
||||
STEP8c_STYLE_LOCK.replace("{{ARTICLE}}", step8b.text),
|
||||
LLM_REFINE
|
||||
);
|
||||
stepsCompleted = 10;
|
||||
stepsCompleted = 11;
|
||||
|
||||
// ═══ STEP 9: QA Check ═══
|
||||
console.log(" Step 11/12: QA Check...");
|
||||
setProgress(draftId, 11, "Step 11/12: QA Check");
|
||||
console.log(" Step 12/13: QA Check...");
|
||||
setProgress(draftId, 12, "Step 12/13: QA Check");
|
||||
const step9 = await generate(systemPrompt,
|
||||
STEP9_QA_CHECK.replace("{{ARTICLE}}", step8c.text),
|
||||
LLM_REFINE
|
||||
);
|
||||
stepsCompleted = 11;
|
||||
stepsCompleted = 12;
|
||||
|
||||
// ═══ STEP 10: Quality Score ═══
|
||||
console.log(" Step 12/12: Quality Score...");
|
||||
setProgress(draftId, 12, "Step 12/12: Quality Score");
|
||||
console.log(" Step 13/13: Quality Score...");
|
||||
setProgress(draftId, 13, "Step 13/13: Quality Score");
|
||||
let autoQaScore: Record<string, unknown> | null = null;
|
||||
try {
|
||||
const step10 = await generate(systemPrompt,
|
||||
@ -1207,7 +1219,32 @@ async function runLlmPipeline(
|
||||
} catch {
|
||||
console.log(" Quality scoring skipped (parse error)");
|
||||
}
|
||||
stepsCompleted = 12;
|
||||
stepsCompleted = 13;
|
||||
|
||||
// ═══ LinkedIn Post ═══
|
||||
console.log(" Step 14/14: LinkedIn Post (max 2,800 chars)...");
|
||||
setProgress(draftId, 14, "Step 14/14: 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),
|
||||
{ temperature: 0.6, maxTokens: 1024, timeoutMs: 120000 }
|
||||
);
|
||||
linkedinPost = stepLinkedIn.text.trim();
|
||||
linkedinCharCount = linkedinPost.length;
|
||||
// Enforce hard limit — truncate at last sentence before 2800 if too long
|
||||
if (linkedinCharCount > 2800) {
|
||||
linkedinPost = linkedinPost.slice(0, 2800).replace(/[^.!?]*$/, "").trim();
|
||||
linkedinCharCount = linkedinPost.length;
|
||||
console.log(` LinkedIn post truncated to ${linkedinCharCount} chars`);
|
||||
} else {
|
||||
console.log(` LinkedIn post: ${linkedinCharCount} chars`);
|
||||
}
|
||||
} catch {
|
||||
console.log(" LinkedIn post generation skipped");
|
||||
}
|
||||
stepsCompleted = 14;
|
||||
|
||||
// Extract only the article from STEP9 output (QA returns review + fixed article)
|
||||
// Look for "COMPLETE FIXED ARTICLE" marker and take everything after it
|
||||
@ -1244,26 +1281,30 @@ async function runLlmPipeline(
|
||||
await pool.query(
|
||||
`UPDATE blog_drafts
|
||||
SET draft_content = $1, word_count = $2,
|
||||
generated_by = 'fo-blog-engine-v4',
|
||||
pipeline_version = 'v4-reduction-stylelock',
|
||||
generated_by = 'fo-blog-engine-v5',
|
||||
pipeline_version = 'v5-narrative-control',
|
||||
pipeline_steps_completed = $3,
|
||||
auto_qa_score = $4,
|
||||
outline = $5,
|
||||
linkedin_post = $6,
|
||||
linkedin_char_count = $7,
|
||||
status = 'draft',
|
||||
updated_at = NOW()
|
||||
WHERE id = $6::uuid`,
|
||||
WHERE id = $8::uuid`,
|
||||
[
|
||||
draftContent,
|
||||
wordCount,
|
||||
stepsCompleted,
|
||||
autoQaScore ? JSON.stringify(autoQaScore) : null,
|
||||
JSON.stringify({
|
||||
generation_method: "fo-pipeline-v3",
|
||||
generation_method: "fo-pipeline-v5",
|
||||
steps_completed: stepsCompleted,
|
||||
blog_type: selectedTopic,
|
||||
quality_issues: finalIssues,
|
||||
feedback_entries_used: feedbackContext ? feedbackContext.split("\n").length : 0,
|
||||
}),
|
||||
linkedinPost,
|
||||
linkedinCharCount,
|
||||
draftId,
|
||||
],
|
||||
);
|
||||
@ -1285,13 +1326,13 @@ async function runLlmPipeline(
|
||||
}
|
||||
|
||||
clearProgress(draftId);
|
||||
console.log(`Blog FO Pipeline: ${draftId} complete — ${wordCount} words, ${stepsCompleted}/10 steps, QA: ${(autoQaScore as any)?.overall || "N/A"}/10`);
|
||||
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`);
|
||||
} catch (llmErr) {
|
||||
clearProgress(draftId);
|
||||
console.warn(`Blog FO Pipeline failed at step ${stepsCompleted + 1}/10 for ${draftId}: ${(llmErr as Error).message}`);
|
||||
console.warn(`Blog FO Pipeline failed at step ${stepsCompleted + 1}/14 for ${draftId}: ${(llmErr as Error).message}`);
|
||||
// Update with partial progress
|
||||
await pool.query(
|
||||
`UPDATE blog_drafts SET pipeline_steps_completed = $1, pipeline_version = 'v3-flexoptix-style',
|
||||
`UPDATE blog_drafts SET pipeline_steps_completed = $1, pipeline_version = 'v5-narrative-control',
|
||||
outline = $2, updated_at = NOW() WHERE id = $3::uuid`,
|
||||
[stepsCompleted, JSON.stringify({ error: (llmErr as Error).message, steps_completed: stepsCompleted }), draftId]
|
||||
).catch(() => {});
|
||||
|
||||
7
sql/024-blog-linkedin-post.sql
Normal file
7
sql/024-blog-linkedin-post.sql
Normal file
@ -0,0 +1,7 @@
|
||||
-- Migration 024: Add linkedin_post column to blog_drafts
|
||||
-- LinkedIn hard limit: 3,000 characters
|
||||
-- Optimal visibility (before "see more"): ~210 chars preview
|
||||
|
||||
ALTER TABLE blog_drafts
|
||||
ADD COLUMN IF NOT EXISTS linkedin_post TEXT,
|
||||
ADD COLUMN IF NOT EXISTS linkedin_char_count INTEGER;
|
||||
Loading…
x
Reference in New Issue
Block a user