From f71ef2b20c2617d7f615e6bad0e71bcbd3b1ade7 Mon Sep 17 00:00:00 2001 From: Rene Fichtmueller Date: Tue, 31 Mar 2026 16:46:25 +0200 Subject: [PATCH] feat(blog): regenerate button, SEO hashtags, calibration engine v2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - POST /api/blog/:id/regenerate — re-runs full 10-step LLM pipeline on existing draft - Regenerate button visible when quality_issues present or status=review - SEO keywords now displayed as clickable #hashtags (copy-to-clipboard) - fo-blog-pipeline: added PoE misuse, DR4 mislabeling, ZR/DR4 conflation as hard QA fails - fo-blog-pipeline: 14 hard rules in system prompt (was 10) - fo-blog-pipeline: CALIBRATION_GOLD_STANDARD + withCalibration() from 10/10 human review - System prompt now includes gold standard example on every pipeline run --- packages/api/src/llm/fo-blog-pipeline.ts | 73 +++++++++++ packages/api/src/routes/blog.ts | 157 ++++++++++++++++++++++- packages/dashboard/index.html | 56 +++++--- 3 files changed, 266 insertions(+), 20 deletions(-) diff --git a/packages/api/src/llm/fo-blog-pipeline.ts b/packages/api/src/llm/fo-blog-pipeline.ts index 8f1e419..9e1535b 100644 --- a/packages/api/src/llm/fo-blog-pipeline.ts +++ b/packages/api/src/llm/fo-blog-pipeline.ts @@ -51,6 +51,11 @@ STRICTLY FORBIDDEN: - Perfect summaries that add nothing - Press release language ("revolutionary", "industry-leading") - Repeating obvious facts +- "PoE budget" or "PoE testing" in ANY optics/transceiver context — PoE = Power over Ethernet (for endpoints). Use "power budget", "power consumption per port", or "thermal headroom" instead. +- "DR4 (Direct Attach)" — DR4 stands for the reach/optical spec (500m SMF), NOT Direct Attach. DAC = Direct Attach Copper. These are completely different things. Never call DR4 "direct attach". +- Treating 400ZR and DR4 as equivalent — they are completely different: DR4 = DC leaf-spine (500m, 8 parallel fibers), ZR = DCI/coherent (80km, single fiber, 15-20W). Always separate them clearly. +- Checklist-style "Final Recommendation" sections — they read like AI. Write as a direct statement, not a bullet list of advice. +- "shiny new toys" or other marketing-speak dismissals at the end — end with something that STICKS HARD RULES (non-negotiable — article FAILS QA without these): 1. Start with a BRUTAL hook — not "If you're still..." but "You're about to sign a PO. Stop." @@ -72,6 +77,10 @@ HARD RULES (non-negotiable — article FAILS QA without these): 8. Reference specific optics (SR4, DR4, FR4, LR4, ZR, etc.) with REAL problems, not just specs 9. Include real numbers (dBm, watts, price per port, cost per Gbit) 10. Cabling reality MUST be addressed: MPO polarity, SR4→DR4 migration fiber count changes, cleaning +11. ENDING MUST HIT: Last sentence must be a hammer. Example: "400G doesn't fail in design. It fails in production. Fast." NOT: "Consider your options carefully." +12. NO CHECKLIST endings: If a section ends with 4 tidy bullet points, rewrite as direct prose. +13. NEVER USE "PoE" in optics context. PoE = Power over Ethernet for endpoints. Use "power consumption per port", "thermal budget", "chassis power envelope" instead. +14. DR4 vs ZR: Always separate. DR4 = leaf-spine (500m), ZR = coherent DCI (80km+). Never treat them as variants of the same thing. REFERENCE VALUES: - SFP+ SR: Tx -8.2 to +0.5 dBm, Rx sensitivity -18.0 dBm, 1.0W typical @@ -337,6 +346,20 @@ QUALITY CHECKS: 9. Would an experienced engineer share this article? Or would they roll their eyes? 10. Is the hook BRUTAL enough? Does it grab in the first 2 sentences? +CALIBRATION FAILS (auto-reject — fix before returning): +11. POE MISUSE: Search for "PoE budget", "PoE testing", "PoE infrastructure" in optics/transceiver context. + → REPLACE with "power budget", "power consumption per port", "thermal headroom", "cooling capacity" +12. DR4 MISLABELING: Search for "DR4 (Direct Attach)" or "DR4 direct attach". + → REPLACE with "DR4 (500m SMF, 8 parallel fibers)" — DR4 is NOT Direct Attach. DAC is Direct Attach Copper. +13. ZR/DR4 CONFLATION: If ZR and DR4 appear together without clear separation, split them: + → "DR4: DC leaf-spine, 500m, parallel optics, 12W | ZR: DCI/coherent, 80-120km, single fiber, 15-20W" +14. CHECKLIST ENDING: If the last section is a 4+ item bullet list, rewrite as 2-3 direct sentences. + → Bad ending: "• Thoroughly Test Your PoE Budget • Invest in Proper Cleaning..." + → Good ending: "400G doesn't fail in design. It fails in production. Plan for the real failure modes, not the vendor's sales slide." +15. HIDDEN COSTS TOO CLEAN: If the hidden costs section feels like a polished table, roughen it. + → Bad: "$350 optic → $2,400 troubleshooting cost" + → Good: "That $350 optic turned into a multi-thousand-dollar problem because someone skipped the connector cleaning." + For each issue: - Quote the problematic text - Explain what's wrong @@ -457,3 +480,53 @@ export function buildFeedbackContext(feedback: Array<{ score: number; feedback_t lines.push("--- END FEEDBACK ---\n"); return lines.join("\n"); } + +// ═══════════════════════════════════════════════════════ +// CALIBRATION REFERENCE — 10/10 Gold Standard +// (Reviewed 2026-03-31, human feedback loop) +// This example teaches the LLM what "production-ready" voice looks like. +// ═══════════════════════════════════════════════════════ + +export const CALIBRATION_GOLD_STANDARD = ` +--- GOLD STANDARD REFERENCE (10/10 — calibrate your output to this level) --- + +KEY STRUCTURAL PATTERNS from a 10/10 article: + +HOOK (correct): +"You're sitting in front of a quote for a few hundred 400G optics. Everything looks clean on paper. Bandwidth solved. Future-proof. Done. +That's usually the moment where things start going wrong." + +WHAT BREAKS (correct — short, direct, no padding): +"Works in lab, fails in production +Classic. +Lab: single vendor, short patch, clean environment. +Production: mixed optics, different firmware, real distances. +Result: CRC errors, unstable links, weird flaps." + +HIDDEN COSTS (correct — raw, not sanitized): +"That 'cheap' optic? Turns into a multi-thousand-euro problem because someone didn't clean a connector. At 400G, contamination isn't a quality issue. It's a service outage." + +CABLING REALITY (correct): +"SR4 to DR4 migration is where budgets go to die. Wrong patch panels, wrong polarity, wrong assumptions. You end up re-cabling things you thought were ready." + +ENDING (correct — hits and stops): +"400G is not risky because it's new. It's risky because people underestimate what actually changes. +If your design only works on paper, it will fail in production. And 400G fails fast." + +WRONG PATTERNS (do not produce these): +❌ "Thoroughly Test Your PoE Budget:" (PoE = wrong context, checklist = wrong format) +❌ "QSFP-DD DR4 (Direct Attach)" (DR4 ≠ Direct Attach) +❌ "DR4 and ZR both push boundaries..." (they serve completely different use cases) +❌ "Don't be swayed by shiny new toys" (marketing speak, not engineer voice) +❌ 4-item bullet recommendation at end (too clean, too AI) + +--- END GOLD STANDARD --- +`; + +/** + * Injects the calibration gold standard into the system prompt. + * Use sparingly — only when available Ollama context allows. + */ +export function withCalibration(systemPrompt: string): string { + return systemPrompt + CALIBRATION_GOLD_STANDARD; +} diff --git a/packages/api/src/routes/blog.ts b/packages/api/src/routes/blog.ts index 2365d5c..d3a3763 100644 --- a/packages/api/src/routes/blog.ts +++ b/packages/api/src/routes/blog.ts @@ -11,6 +11,18 @@ */ import { Router, Request, Response } from "express"; import { pool } from "../db/client"; + +/** In-memory pipeline progress tracker — step updates pushed here, polled via GET /api/blog/:id/progress */ +const pipelineProgress = new Map(); + +function setProgress(draftId: string, step: number, label: string): void { + const pct = Math.round((step / 10) * 92) + 2; // 2%..94% during run, 100% on complete + pipelineProgress.set(draftId, { step, total: 10, label, pct }); +} + +function clearProgress(draftId: string): void { + pipelineProgress.delete(draftId); +} import { semanticSearch } from "../embeddings/client"; import { generate, checkHealth } from "../llm/client"; import { @@ -98,6 +110,76 @@ const BLOG_TEMPLATES: Record = { seo_keywords: ["transceiver buying guide", "how to choose transceiver", "form factor guide"], }, ], + technology_deep_dive: [ + { + topic: "technology_deep_dive", + title: "Deep Dive: {SPEED} Technology — What the Specs Don't Tell You", + target_audience: "technical", + seo_keywords: ["optical transceiver technology", "deep dive", "silicon photonics", "coherent optics"], + }, + { + topic: "technology_deep_dive", + title: "{YEAR} Standards Roundup: What's Actually Production-Ready", + target_audience: "technical", + seo_keywords: ["IEEE 802.3", "OIF standards", "MSA", "production optics"], + }, + ], + market_alert: [ + { + topic: "market_alert", + title: "Market Alert: {SPEED} Transceiver Prices Are Moving — Here's Why", + target_audience: "sales", + seo_keywords: ["transceiver price", "market analysis", "optical networking market", "price drop"], + }, + { + topic: "market_alert", + title: "Price War: What {YEAR}'s Transceiver Market Shift Means for Your Budget", + target_audience: "sales", + seo_keywords: ["transceiver market", "price trend", "optical module cost", "procurement"], + }, + ], + migration_guide: [ + { + topic: "migration_guide", + title: "The Complete Migration Guide: Moving to {SPEED} Without Breaking Production", + target_audience: "technical", + seo_keywords: ["network migration", "transceiver upgrade", "100G to 400G", "migration guide"], + }, + { + topic: "migration_guide", + title: "{YEAR} Migration Playbook: From Planning to Production in 12 Months", + target_audience: "technical", + seo_keywords: ["network upgrade", "migration planning", "optical transceiver migration"], + }, + ], + buying_guide: [ + { + topic: "buying_guide", + title: "The {YEAR} Transceiver Buying Guide: What to Buy, What to Skip", + target_audience: "customer", + seo_keywords: ["transceiver buying guide", "best transceiver", "OEM vs compatible", "procurement"], + }, + { + topic: "buying_guide", + title: "OEM vs Compatible Transceivers in {YEAR}: The Real Numbers", + target_audience: "customer", + seo_keywords: ["OEM transceiver", "compatible transceiver", "cost savings", "Flexoptix"], + }, + ], + competitor_analysis: [ + { + topic: "competitor_analysis", + title: "Competitor Roundup: What's New in {SPEED} Transceivers and What It Means", + target_audience: "sales", + seo_keywords: ["transceiver comparison", "competitor analysis", "optical module vendors"], + }, + { + topic: "competitor_analysis", + title: "{YEAR} Vendor Landscape: Who's Winning the {FORM_FACTOR} Market", + target_audience: "sales", + seo_keywords: ["transceiver vendor", "market share", "optical networking vendors"], + }, + ], }; /** Gather data from vector collections for blog content — with PostgreSQL fallback. @@ -857,6 +939,7 @@ async function runLlmPipeline( STEP10_QUALITY_SCORE, BLOG_TYPES, buildFeedbackContext, + withCalibration, } = await import("../llm/fo-blog-pipeline"); const LLM_OPTS = { temperature: 0.7, maxTokens: 6144, timeoutMs: 480000 }; @@ -880,7 +963,7 @@ async function runLlmPipeline( }))); } catch { /* no feedback yet, that's fine */ } - const systemPrompt = FO_BLOG_SYSTEM_PROMPT + feedbackContext; + const systemPrompt = withCalibration(FO_BLOG_SYSTEM_PROMPT + feedbackContext); // Warmup await generate("Test", "OK", { temperature: 0.1, maxTokens: 8, timeoutMs: 60000 }).catch(() => {}); @@ -895,6 +978,7 @@ async function runLlmPipeline( // ═══ STEP 1: Topic Expansion ═══ console.log(" Step 1/10: Topic Expansion..."); + setProgress(draftId, 1, "Step 1/10: Topic Expansion"); const step1 = await generate(systemPrompt, STEP1_TOPIC_EXPANSION.replace("{{TOPIC}}", title), LLM_OPTS @@ -903,6 +987,7 @@ async function runLlmPipeline( // ═══ STEP 2: Angle Selection ═══ console.log(" Step 2/10: Angle Selection..."); + setProgress(draftId, 2, "Step 2/10: Angle Selection"); const step2 = await generate(systemPrompt, STEP2_ANGLE_SELECTION.replace("{{SCENARIOS}}", step1.text), LLM_REFINE @@ -911,6 +996,7 @@ async function runLlmPipeline( // ═══ STEP 3: Outline ═══ console.log(" Step 3/10: Outline Generation..."); + setProgress(draftId, 3, "Step 3/10: Outline Generation"); const step3 = await generate(systemPrompt, STEP3_OUTLINE .replace("{{ANGLE}}", step2.text) @@ -922,6 +1008,7 @@ async function runLlmPipeline( // ═══ STEP 4: Master Draft ═══ console.log(" Step 4/10: Master Draft (this takes a while)..."); + setProgress(draftId, 4, "Step 4/10: Master Draft (longest step…)"); const step4 = await generate(systemPrompt, STEP4_MASTER_DRAFT .replace("{{OUTLINE}}", step3.text) @@ -933,6 +1020,7 @@ async function runLlmPipeline( // ═══ 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), LLM_REFINE @@ -941,6 +1029,7 @@ async function runLlmPipeline( // ═══ STEP 6: Technical Deepening ═══ console.log(" Step 6/10: Technical Deepening..."); + setProgress(draftId, 6, "Step 6/10: Technical Deepening"); const step6 = await generate(systemPrompt, STEP6_TECHNICAL_DEEPENING.replace("{{ARTICLE}}", step5.text), LLM_REFINE @@ -949,6 +1038,7 @@ async function runLlmPipeline( // ═══ STEP 7: Opinion Layer ═══ console.log(" Step 7/10: Opinion Layer..."); + setProgress(draftId, 7, "Step 7/10: Opinion Layer"); const step7 = await generate(systemPrompt, STEP7_OPINION_LAYER.replace("{{ARTICLE}}", step6.text), LLM_REFINE @@ -957,6 +1047,7 @@ async function runLlmPipeline( // ═══ STEP 8: Kill AI Tone ═══ console.log(" Step 8/10: Kill AI Tone..."); + setProgress(draftId, 8, "Step 8/10: Kill AI Tone"); const step8 = await generate(systemPrompt, STEP8_KILL_AI_TONE.replace("{{ARTICLE}}", step7.text), LLM_REFINE @@ -965,6 +1056,7 @@ async function runLlmPipeline( // ═══ STEP 9: QA Check ═══ console.log(" Step 9/10: QA Check..."); + setProgress(draftId, 9, "Step 9/10: QA Check"); const step9 = await generate(systemPrompt, STEP9_QA_CHECK.replace("{{ARTICLE}}", step8.text), LLM_REFINE @@ -973,6 +1065,7 @@ async function runLlmPipeline( // ═══ STEP 10: Quality Score ═══ console.log(" Step 10/10: Quality Score..."); + setProgress(draftId, 10, "Step 10/10: Quality Score"); let autoQaScore: Record | null = null; try { const step10 = await generate(systemPrompt, @@ -1039,8 +1132,10 @@ async function runLlmPipeline( ).catch(() => {}); } + clearProgress(draftId); console.log(`Blog FO Pipeline: ${draftId} complete — ${wordCount} words, ${stepsCompleted}/10 steps, QA: ${(autoQaScore as any)?.overall || "N/A"}/10`); } catch (llmErr) { + clearProgress(draftId); console.warn(`Blog FO Pipeline failed at step ${stepsCompleted + 1}/10 for ${draftId}: ${(llmErr as Error).message}`); // Update with partial progress await pool.query( @@ -1181,6 +1276,16 @@ blogRouter.get("/", async (_req: Request, res: Response) => { }); // GET /api/blog/:id — Get a specific draft with full content +// GET /api/blog/:id/progress — Real-time pipeline step progress (in-memory) +blogRouter.get("/:id/progress", (req: Request, res: Response) => { + const p = pipelineProgress.get(req.params.id); + if (!p) { + res.json({ success: true, running: false, step: 0, total: 10, label: "Idle", pct: 0 }); + return; + } + res.json({ success: true, running: true, ...p }); +}); + blogRouter.get("/:id", async (req: Request, res: Response) => { try { const result = await pool.query( @@ -1292,6 +1397,56 @@ blogRouter.get("/feedback/training-data", async (_req: Request, res: Response) = } catch (err) { res.status(500).json({ error: "Failed" }); } }); +// POST /api/blog/:id/regenerate — Re-run full LLM pipeline on existing draft (for review/quality-issue cases) +blogRouter.post("/:id/regenerate", async (req: Request, res: Response) => { + try { + const result = await pool.query( + `SELECT id, title, topic, target_audience, seo_keywords FROM blog_drafts WHERE id = $1::uuid`, + [req.params.id], + ); + + if (result.rows.length === 0) { + res.status(404).json({ success: false, error: "Draft not found" }); + return; + } + + const draft = result.rows[0]; + const keywords: string[] = draft.seo_keywords || []; + + // Re-gather fresh data for this topic + const data = await gatherBlogData(keywords, draft.topic); + + // Reset status to draft + clear quality issues in outline + await pool.query( + `UPDATE blog_drafts SET status = 'draft', updated_at = NOW(), + outline = outline || '{"quality_issues":[],"regeneration_requested":true}'::jsonb + WHERE id = $1::uuid`, + [draft.id], + ); + + // Check LLM availability + const health = await checkHealth().catch(() => ({ ok: false, model: "", error: "unreachable" })); + if (!health.ok) { + res.status(503).json({ success: false, error: "LLM not available — cannot regenerate" }); + return; + } + + console.log(`Blog Regenerate: Re-queuing LLM pipeline for draft ${draft.id} ("${draft.title}")`); + enqueueLlmPipeline(draft.id, draft.title, draft.topic, draft.target_audience, data).catch((err) => { + console.error(`Blog regenerate pipeline error: ${(err as Error).message}`); + }); + + res.json({ + success: true, + draft_id: draft.id, + title: draft.title, + message: "LLM pipeline re-queued — poll /api/blog/:id/progress for status", + }); + } catch (err) { + res.status(500).json({ success: false, error: (err as Error).message }); + } +}); + // DELETE /api/blog/:id — Delete a blog draft blogRouter.delete("/:id", async (req: Request, res: Response) => { try { diff --git a/packages/dashboard/index.html b/packages/dashboard/index.html index a903615..db9c387 100644 --- a/packages/dashboard/index.html +++ b/packages/dashboard/index.html @@ -794,18 +794,6 @@ - -
-
5-Year Adoption Forecast
-
-
- - -
-
Regional Adoption Heatmap
-
-
- @@ -880,6 +868,7 @@
Loading topics...
+
@@ -2324,10 +2313,13 @@ async function loadBlogDrafts() { var sc = d.status === 'published' ? 'b-green' : d.status === 'review' ? 'b-yellow' : 'b-blue'; var gen = (d.generated_by || '').replace('tip-blog-engine-', ''); var gc = gen === 'llm' ? 'b-green' : gen === 'template-fallback' ? 'b-yellow' : 'b-neutral'; - return '
' + return '
' + '
' + '
' + esc(d.title) + '
' + + '
' + '' + esc(d.status) + '' + + '' + + '
' + '
' + '
' + '' + esc(d.topic) + '' @@ -2354,9 +2346,11 @@ async function openBlogDetail(id) { h += '' + esc(genMethod || 'template') + ' '; h += '' + esc(d.word_count) + ' words'; h += '
'; - if (outline.quality_issues && outline.quality_issues.length > 0) { - h += '
'; - h += 'Quality issues: ' + outline.quality_issues.map(esc).join(', '); + var hasQualityIssues = outline.quality_issues && outline.quality_issues.length > 0; + if (hasQualityIssues) { + h += '
'; + h += '
Quality issues: ' + outline.quality_issues.map(esc).join(', ') + '
'; + h += ''; h += '
'; } @@ -2367,9 +2361,15 @@ async function openBlogDetail(id) { h += '
'; h += '
' + mdToHtml(d.draft_content) + '
'; - h += '
SEO Keywords
'; - h += '
' + (d.seo_keywords || []).map(function(k) { return '' + esc(k) + ''; }).join('') + '
'; - h += '
'; + h += '
SEO Keywords / Hashtags
'; + h += '
' + (d.seo_keywords || []).map(function(k) { + var tag = '#' + k.replace(/\s+/g, ''); + return '' + esc(tag) + ''; + }).join('') + '
'; + h += '
'; + if (d.status === 'review' || hasQualityIssues) { + h += ''; + } h += ''; h += ''; h += ''; @@ -2393,6 +2393,24 @@ async function updateBlogStatus(id, status) { } catch(e) { showToast('Error', e.message, true); } } +async function regenerateBlog(id) { + showToast('Regenerating…', 'LLM pipeline wird neu gestartet'); + try { + var data = await fetch(API + '/api/blog/' + id + '/regenerate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }).then(function(r) { return r.json(); }); + if (data.success) { + showToast('Gestartet', 'LLM läuft – Status wird aktualisiert'); + loadBlogDrafts(); + // Poll for completion + pollBlogLlm(id, 0); + } else { + showToast('Fehler', data.error || 'Regenerierung fehlgeschlagen', true); + } + } catch(e) { showToast('Error', e.message, true); } +} + // TABLE SORTING function makeSortable(table) { if (!table) return;