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.
|
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:
|
YOUR MINDSET:
|
||||||
- You write like an engineer at 2:17 AM in the DC, not like a marketing department
|
- 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
|
- You base everything on real problems, not spec sheets
|
||||||
@ -356,7 +382,10 @@ CONTENT APPROACH:
|
|||||||
- Every number gets context (deployment size, vendor type, conditions)
|
- Every number gets context (deployment size, vendor type, conditions)
|
||||||
- Max 3-4 core ideas — pick the best and develop them through experience
|
- 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 from Flexoptix database (verified — use exactly as provided):
|
||||||
{{CONTEXT_DATA}}
|
{{CONTEXT_DATA}}
|
||||||
@ -364,6 +393,91 @@ Context data from Flexoptix database (verified — use exactly as provided):
|
|||||||
Outline:
|
Outline:
|
||||||
{{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
|
// 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
|
- Ending: the cost is lost time, stated simply and directly
|
||||||
- ZERO section headers, ZERO bullet lists, ZERO numbered steps
|
- 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):
|
WRONG PATTERNS (both styles — never produce):
|
||||||
❌ "Thoroughly Test Your PoE Budget:" (PoE = wrong context, checklist = wrong format)
|
❌ "Thoroughly Test Your PoE Budget:" (PoE = wrong context, checklist = wrong format)
|
||||||
❌ "QSFP-DD DR4 (Direct Attach)" (DR4 ≠ Direct Attach — DAC is Direct Attach Copper)
|
❌ "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,
|
// (2026-04-04: Added based on field feedback — articles were too long,
|
||||||
// repeated concepts, and "assembled" rather than written)
|
// 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.
|
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:
|
WHAT TO REMOVE:
|
||||||
- Any concept explained more than once (pick its best version, cut the rest)
|
- 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 }>();
|
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 / 12) * 92) + 2; // 2%..94% during run, 100% on complete
|
const pct = Math.round((step / 14) * 92) + 2; // 2%..94% during run, 100% on complete
|
||||||
pipelineProgress.set(draftId, { step, total: 12, label, pct });
|
pipelineProgress.set(draftId, { step, total: 14, label, pct });
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearProgress(draftId: string): void {
|
function clearProgress(draftId: string): void {
|
||||||
@ -997,6 +997,7 @@ async function runLlmPipeline(
|
|||||||
STEP2_ANGLE_SELECTION,
|
STEP2_ANGLE_SELECTION,
|
||||||
STEP3_OUTLINE,
|
STEP3_OUTLINE,
|
||||||
STEP4_MASTER_DRAFT,
|
STEP4_MASTER_DRAFT,
|
||||||
|
STEP4b_NARRATIVE_CONTROL,
|
||||||
STEP5_REALITY_INJECTION,
|
STEP5_REALITY_INJECTION,
|
||||||
STEP6_TECHNICAL_DEEPENING,
|
STEP6_TECHNICAL_DEEPENING,
|
||||||
STEP7_OPINION_LAYER,
|
STEP7_OPINION_LAYER,
|
||||||
@ -1005,14 +1006,15 @@ async function runLlmPipeline(
|
|||||||
STEP8c_STYLE_LOCK,
|
STEP8c_STYLE_LOCK,
|
||||||
STEP9_QA_CHECK,
|
STEP9_QA_CHECK,
|
||||||
STEP10_QUALITY_SCORE,
|
STEP10_QUALITY_SCORE,
|
||||||
|
STEP_LINKEDIN_POST,
|
||||||
BLOG_TYPES,
|
BLOG_TYPES,
|
||||||
buildFeedbackContext,
|
buildFeedbackContext,
|
||||||
withCalibration,
|
withCalibration,
|
||||||
} = await import("../llm/fo-blog-pipeline");
|
} = 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 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;
|
let stepsCompleted = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -1125,18 +1127,28 @@ async function runLlmPipeline(
|
|||||||
stepsCompleted = 4;
|
stepsCompleted = 4;
|
||||||
console.log(` Draft: ${step4.text.split(/\s+/).length} words`);
|
console.log(` Draft: ${step4.text.split(/\s+/).length} words`);
|
||||||
|
|
||||||
// ═══ STEP 5: Reality Injection ═══
|
// ═══ STEP 4b: Narrative Control ═══
|
||||||
console.log(" Step 5/10: Reality Injection...");
|
console.log(" Step 5/13: Narrative Control (framing check + anti-FUD)...");
|
||||||
setProgress(draftId, 5, "Step 5/10: Reality Injection");
|
setProgress(draftId, 5, "Step 5/13: Narrative Control");
|
||||||
const step5 = await generate(systemPrompt,
|
const step4b = await generate(systemPrompt,
|
||||||
STEP5_REALITY_INJECTION.replace("{{DRAFT}}", step4.text),
|
STEP4b_NARRATIVE_CONTROL.replace("{{ARTICLE}}", step4.text),
|
||||||
LLM_REFINE
|
LLM_REFINE
|
||||||
);
|
);
|
||||||
stepsCompleted = 5;
|
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 ═══
|
// ═══ STEP 6: Technical Deepening ═══
|
||||||
console.log(" Step 6/10: Technical Deepening...");
|
console.log(" Step 7/13: Technical Deepening...");
|
||||||
setProgress(draftId, 6, "Step 6/10: Technical Deepening");
|
setProgress(draftId, 7, "Step 7/13: Technical Deepening");
|
||||||
const step6 = await generate(systemPrompt,
|
const step6 = await generate(systemPrompt,
|
||||||
STEP6_TECHNICAL_DEEPENING.replace("{{ARTICLE}}", step5.text),
|
STEP6_TECHNICAL_DEEPENING.replace("{{ARTICLE}}", step5.text),
|
||||||
LLM_REFINE
|
LLM_REFINE
|
||||||
@ -1144,54 +1156,54 @@ async function runLlmPipeline(
|
|||||||
stepsCompleted = 6;
|
stepsCompleted = 6;
|
||||||
|
|
||||||
// ═══ STEP 7: Opinion Layer ═══
|
// ═══ STEP 7: Opinion Layer ═══
|
||||||
console.log(" Step 7/10: Opinion Layer...");
|
console.log(" Step 8/13: Opinion Layer...");
|
||||||
setProgress(draftId, 7, "Step 7/10: Opinion Layer");
|
setProgress(draftId, 8, "Step 8/13: Opinion Layer");
|
||||||
const step7 = await generate(systemPrompt,
|
const step7 = await generate(systemPrompt,
|
||||||
STEP7_OPINION_LAYER.replace("{{ARTICLE}}", step6.text),
|
STEP7_OPINION_LAYER.replace("{{ARTICLE}}", step6.text),
|
||||||
LLM_REFINE
|
LLM_REFINE
|
||||||
);
|
);
|
||||||
stepsCompleted = 7;
|
stepsCompleted = 8;
|
||||||
|
|
||||||
// ═══ STEP 8: Kill AI Tone ═══
|
// ═══ STEP 8: Kill AI Tone ═══
|
||||||
console.log(" Step 8/10: Kill AI Tone...");
|
console.log(" Step 9/13: Kill AI Tone...");
|
||||||
setProgress(draftId, 8, "Step 8/10: Kill AI Tone");
|
setProgress(draftId, 9, "Step 9/13: Kill AI Tone");
|
||||||
const step8 = await generate(systemPrompt,
|
const step8 = await generate(systemPrompt,
|
||||||
STEP8_KILL_AI_TONE.replace("{{ARTICLE}}", step7.text),
|
STEP8_KILL_AI_TONE.replace("{{ARTICLE}}", step7.text),
|
||||||
LLM_REFINE
|
LLM_REFINE
|
||||||
);
|
);
|
||||||
stepsCompleted = 8;
|
stepsCompleted = 9;
|
||||||
|
|
||||||
// ═══ STEP 8b: Reduction Pass ═══
|
// ═══ STEP 8b: Reduction Pass ═══
|
||||||
console.log(" Step 9/12: Reduction Pass (remove 25-35%)...");
|
console.log(" Step 10/13: Reduction Pass (remove 15-25%, keep ≥1500 words)...");
|
||||||
setProgress(draftId, 9, "Step 9/12: Reduction Pass");
|
setProgress(draftId, 10, "Step 10/13: Reduction Pass");
|
||||||
const step8b = await generate(systemPrompt,
|
const step8b = await generate(systemPrompt,
|
||||||
STEP8b_REDUCTION.replace("{{ARTICLE}}", step8.text),
|
STEP8b_REDUCTION.replace("{{ARTICLE}}", step8.text),
|
||||||
LLM_REFINE
|
LLM_REFINE
|
||||||
);
|
);
|
||||||
stepsCompleted = 9;
|
stepsCompleted = 10;
|
||||||
console.log(` After reduction: ${step8b.text.split(/\s+/).length} words (was ${step8.text.split(/\s+/).length})`);
|
console.log(` After reduction: ${step8b.text.split(/\s+/).length} words (was ${step8.text.split(/\s+/).length})`);
|
||||||
|
|
||||||
// ═══ STEP 8c: Style Lock ═══
|
// ═══ STEP 8c: Style Lock ═══
|
||||||
console.log(" Step 10/12: Style Lock (tone consistency + scope/SKU fixes)...");
|
console.log(" Step 11/13: Style Lock (tone consistency + scope/SKU fixes)...");
|
||||||
setProgress(draftId, 10, "Step 10/12: Style Lock");
|
setProgress(draftId, 11, "Step 11/13: Style Lock");
|
||||||
const step8c = await generate(systemPrompt,
|
const step8c = await generate(systemPrompt,
|
||||||
STEP8c_STYLE_LOCK.replace("{{ARTICLE}}", step8b.text),
|
STEP8c_STYLE_LOCK.replace("{{ARTICLE}}", step8b.text),
|
||||||
LLM_REFINE
|
LLM_REFINE
|
||||||
);
|
);
|
||||||
stepsCompleted = 10;
|
stepsCompleted = 11;
|
||||||
|
|
||||||
// ═══ STEP 9: QA Check ═══
|
// ═══ STEP 9: QA Check ═══
|
||||||
console.log(" Step 11/12: QA Check...");
|
console.log(" Step 12/13: QA Check...");
|
||||||
setProgress(draftId, 11, "Step 11/12: QA Check");
|
setProgress(draftId, 12, "Step 12/13: QA Check");
|
||||||
const step9 = await generate(systemPrompt,
|
const step9 = await generate(systemPrompt,
|
||||||
STEP9_QA_CHECK.replace("{{ARTICLE}}", step8c.text),
|
STEP9_QA_CHECK.replace("{{ARTICLE}}", step8c.text),
|
||||||
LLM_REFINE
|
LLM_REFINE
|
||||||
);
|
);
|
||||||
stepsCompleted = 11;
|
stepsCompleted = 12;
|
||||||
|
|
||||||
// ═══ STEP 10: Quality Score ═══
|
// ═══ STEP 10: Quality Score ═══
|
||||||
console.log(" Step 12/12: Quality Score...");
|
console.log(" Step 13/13: Quality Score...");
|
||||||
setProgress(draftId, 12, "Step 12/12: Quality Score");
|
setProgress(draftId, 13, "Step 13/13: Quality Score");
|
||||||
let autoQaScore: Record<string, unknown> | null = null;
|
let autoQaScore: Record<string, unknown> | null = null;
|
||||||
try {
|
try {
|
||||||
const step10 = await generate(systemPrompt,
|
const step10 = await generate(systemPrompt,
|
||||||
@ -1207,7 +1219,32 @@ async function runLlmPipeline(
|
|||||||
} catch {
|
} catch {
|
||||||
console.log(" Quality scoring skipped (parse error)");
|
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)
|
// Extract only the article from STEP9 output (QA returns review + fixed article)
|
||||||
// Look for "COMPLETE FIXED ARTICLE" marker and take everything after it
|
// Look for "COMPLETE FIXED ARTICLE" marker and take everything after it
|
||||||
@ -1244,26 +1281,30 @@ 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-v4',
|
generated_by = 'fo-blog-engine-v5',
|
||||||
pipeline_version = 'v4-reduction-stylelock',
|
pipeline_version = 'v5-narrative-control',
|
||||||
pipeline_steps_completed = $3,
|
pipeline_steps_completed = $3,
|
||||||
auto_qa_score = $4,
|
auto_qa_score = $4,
|
||||||
outline = $5,
|
outline = $5,
|
||||||
|
linkedin_post = $6,
|
||||||
|
linkedin_char_count = $7,
|
||||||
status = 'draft',
|
status = 'draft',
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $6::uuid`,
|
WHERE id = $8::uuid`,
|
||||||
[
|
[
|
||||||
draftContent,
|
draftContent,
|
||||||
wordCount,
|
wordCount,
|
||||||
stepsCompleted,
|
stepsCompleted,
|
||||||
autoQaScore ? JSON.stringify(autoQaScore) : null,
|
autoQaScore ? JSON.stringify(autoQaScore) : null,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
generation_method: "fo-pipeline-v3",
|
generation_method: "fo-pipeline-v5",
|
||||||
steps_completed: stepsCompleted,
|
steps_completed: stepsCompleted,
|
||||||
blog_type: selectedTopic,
|
blog_type: selectedTopic,
|
||||||
quality_issues: finalIssues,
|
quality_issues: finalIssues,
|
||||||
feedback_entries_used: feedbackContext ? feedbackContext.split("\n").length : 0,
|
feedback_entries_used: feedbackContext ? feedbackContext.split("\n").length : 0,
|
||||||
}),
|
}),
|
||||||
|
linkedinPost,
|
||||||
|
linkedinCharCount,
|
||||||
draftId,
|
draftId,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -1285,13 +1326,13 @@ async function runLlmPipeline(
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearProgress(draftId);
|
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) {
|
} catch (llmErr) {
|
||||||
clearProgress(draftId);
|
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
|
// Update with partial progress
|
||||||
await pool.query(
|
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`,
|
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(() => {});
|
||||||
|
|||||||
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