From 98a7e1228253c25baf05251f3571723947d17372 Mon Sep 17 00:00:00 2001 From: Rene Fichtmueller Date: Tue, 31 Mar 2026 09:16:23 +0200 Subject: [PATCH] feat: wire 10-step FO Blog Pipeline into blog generation route Replaces old 2-pass pipeline with full Flexoptix Style 10-step generation: 1. Topic Expansion (real scenarios + wrong assumptions) 2. Angle Selection (single strong angle + audience) 3. Outline Generation (decision-driven, no generic sections) 4. Master Draft (Flexoptix voice, 2000+ words) 5. Reality Injection (failure scenarios, operational pain) 6. Technical Deepening (specific optics, power, density) 7. Opinion Layer (clear positions, no neutrality) 8. Kill AI Tone (remove all AI fingerprints) 9. QA Check (technical accuracy verification) 10. Quality Score (1-10 auto-rating, saved as self-feedback) Feedback loop active: - Accumulated feedback injected into system prompt - Auto QA scores saved to blog_feedback table - Training data export via GET /api/blog/feedback/training-data --- packages/api/src/routes/blog.ts | 247 +++++++++++++++++++++++--------- 1 file changed, 182 insertions(+), 65 deletions(-) diff --git a/packages/api/src/routes/blog.ts b/packages/api/src/routes/blog.ts index 51140f4..7aa38c6 100644 --- a/packages/api/src/routes/blog.ts +++ b/packages/api/src/routes/blog.ts @@ -834,7 +834,7 @@ async function processLlmQueue(): Promise { if (llmQueue.length > 0) processLlmQueue(); } -/** Run LLM pipeline and update draft in-place */ +/** Run 10-Step Flexoptix Style LLM Pipeline and update draft in-place */ async function runLlmPipeline( draftId: string, title: string, @@ -842,95 +842,212 @@ async function runLlmPipeline( targetAudience: string, data: Awaited>, ): Promise { + // Lazy-load the new FO pipeline + const { + FO_BLOG_SYSTEM_PROMPT, + STEP1_TOPIC_EXPANSION, + STEP2_ANGLE_SELECTION, + STEP3_OUTLINE, + STEP4_MASTER_DRAFT, + STEP5_REALITY_INJECTION, + STEP6_TECHNICAL_DEEPENING, + STEP7_OPINION_LAYER, + STEP8_KILL_AI_TONE, + STEP9_QA_CHECK, + STEP10_QUALITY_SCORE, + BLOG_TYPES, + buildFeedbackContext, + } = await import("../llm/fo-blog-pipeline"); + + const LLM_OPTS = { temperature: 0.7, maxTokens: 6144, timeoutMs: 480000 }; + const LLM_REFINE = { temperature: 0.4, maxTokens: 6144, timeoutMs: 480000 }; + let stepsCompleted = 0; + try { - console.log(`Blog LLM: Starting pipeline for ${draftId}`); + console.log(`Blog FO Pipeline: Starting 10-step generation for ${draftId}`); + console.log(` Topic: "${title}" | Type: ${selectedTopic} | Audience: ${targetAudience}`); - // Warmup: tiny prompt to ensure model is loaded in memory - await generate("You are a test.", "Reply OK.", { - temperature: 0.1, - maxTokens: 8, - timeoutMs: 60000, - }).catch(() => { /* warmup failure is non-fatal */ }); + // Load accumulated feedback to inject into system prompt + let feedbackContext = ""; + try { + const fbResult = await pool.query( + `SELECT score_overall, feedback_text, blog_type FROM blog_feedback + WHERE feedback_text IS NOT NULL AND feedback_text != '' + ORDER BY score_overall ASC LIMIT 20` + ); + feedbackContext = buildFeedbackContext(fbResult.rows.map(r => ({ + score: r.score_overall, feedback_text: r.feedback_text, blog_type: r.blog_type || "" + }))); + } catch { /* no feedback yet, that's fine */ } - // Pass 1: Master generation with full structure enforcement - const topicPrompt = buildTopicPrompt(selectedTopic, data); - const pass1 = await generate(SYSTEM_PROMPT, `Title: "${title}"\n\n${topicPrompt}`, { - temperature: 0.7, - maxTokens: 6144, - timeoutMs: 480000, - }); - console.log(` Pass 1 (master): ${pass1.evalCount} tokens`); + const systemPrompt = FO_BLOG_SYSTEM_PROMPT + feedbackContext; - // Check intro quality — merge anti-generic into depth pass if needed - const introCheck = pass1.text.split("\n").slice(0, 10).join("\n").toLowerCase(); - const needsIntroFix = - introCheck.includes("the optical transceiver market") || - introCheck.includes("in today") || - introCheck.includes("increasingly") || - introCheck.includes("plays a key role"); + // Warmup + await generate("Test", "OK", { temperature: 0.1, maxTokens: 8, timeoutMs: 60000 }).catch(() => {}); - // Pass 2: Combined depth + quality + intro fix - const issues = validateArticle(pass1.text); - const combinedPrompt = [ - DEPTH_PROMPT, - "", - "ADDITIONALLY, apply these quality controls:", - QUALITY_CONTROL_PROMPT, - needsIntroFix ? `\nALSO: ${ANTI_GENERIC_INTRO_PROMPT}` : "", - issues.length > 0 ? `\nDETECTED ISSUES TO FIX: ${issues.join("; ")}` : "", - "", - "--- ARTICLE TO IMPROVE ---", - "", - pass1.text, - ].join("\n"); + // Build context data string for injection + const contextData = data.products.slice(0, 15).map(p => + `${p.standard_name || p.slug}: ${p.form_factor} ${p.speed}, reach ${p.reach_label || "N/A"}, fiber ${p.fiber_type || "N/A"}, vendor ${p.vendor || "N/A"}${p.price ? `, ~€${p.price}` : ""}` + ).join("\n"); - const pass2 = await generate(SYSTEM_PROMPT, combinedPrompt, { - temperature: 0.4, - maxTokens: 6144, - timeoutMs: 480000, - }); - console.log(` Pass 2 (depth+quality): ${pass2.evalCount} tokens`); - if (needsIntroFix) console.log(" (included intro fix)"); - if (issues.length > 0) console.log(` Quality issues fixed: ${issues.join(", ")}`); + // Get blog type config + const blogType = BLOG_TYPES[selectedTopic as keyof typeof BLOG_TYPES] || BLOG_TYPES.tutorial; - let draftContent = `# ${title}\n\n${pass2.text}`; + // ═══ STEP 1: Topic Expansion ═══ + console.log(" Step 1/10: Topic Expansion..."); + const step1 = await generate(systemPrompt, + STEP1_TOPIC_EXPANSION.replace("{{TOPIC}}", title), + LLM_OPTS + ); + stepsCompleted = 1; - // Optional: Add procurement notes for sales/customer audience - if (targetAudience === "sales" || targetAudience === "customer") { - try { - const procPass = await generate(SYSTEM_PROMPT, `${PROCUREMENT_LAYER_PROMPT}\n\n--- ARTICLE ---\n\n${draftContent}`, { - temperature: 0.4, - maxTokens: 4096, - timeoutMs: 240000, - }); - draftContent = procPass.text; - console.log(` Procurement layer: ${procPass.evalCount} tokens`); - } catch { - console.log(" Procurement pass skipped (timeout)"); + // ═══ STEP 2: Angle Selection ═══ + console.log(" Step 2/10: Angle Selection..."); + const step2 = await generate(systemPrompt, + STEP2_ANGLE_SELECTION.replace("{{SCENARIOS}}", step1.text), + LLM_REFINE + ); + stepsCompleted = 2; + + // ═══ STEP 3: Outline ═══ + console.log(" Step 3/10: Outline Generation..."); + const step3 = await generate(systemPrompt, + STEP3_OUTLINE + .replace("{{ANGLE}}", step2.text) + .replace("{{AUDIENCE}}", targetAudience) + .replace("{{DECISION}}", title), + LLM_REFINE + ); + stepsCompleted = 3; + + // ═══ STEP 4: Master Draft ═══ + console.log(" Step 4/10: Master Draft (this takes a while)..."); + const step4 = await generate(systemPrompt, + STEP4_MASTER_DRAFT + .replace("{{OUTLINE}}", step3.text) + .replace("{{CONTEXT_DATA}}", contextData), + { ...LLM_OPTS, maxTokens: 8192 } + ); + stepsCompleted = 4; + console.log(` Draft: ${step4.text.split(/\s+/).length} words`); + + // ═══ STEP 5: Reality Injection ═══ + console.log(" Step 5/10: Reality Injection..."); + const step5 = await generate(systemPrompt, + STEP5_REALITY_INJECTION.replace("{{DRAFT}}", step4.text), + LLM_REFINE + ); + stepsCompleted = 5; + + // ═══ STEP 6: Technical Deepening ═══ + console.log(" Step 6/10: Technical Deepening..."); + const step6 = await generate(systemPrompt, + STEP6_TECHNICAL_DEEPENING.replace("{{ARTICLE}}", step5.text), + LLM_REFINE + ); + stepsCompleted = 6; + + // ═══ STEP 7: Opinion Layer ═══ + console.log(" Step 7/10: Opinion Layer..."); + const step7 = await generate(systemPrompt, + STEP7_OPINION_LAYER.replace("{{ARTICLE}}", step6.text), + LLM_REFINE + ); + stepsCompleted = 7; + + // ═══ STEP 8: Kill AI Tone ═══ + console.log(" Step 8/10: Kill AI Tone..."); + const step8 = await generate(systemPrompt, + STEP8_KILL_AI_TONE.replace("{{ARTICLE}}", step7.text), + LLM_REFINE + ); + stepsCompleted = 8; + + // ═══ STEP 9: QA Check ═══ + console.log(" Step 9/10: QA Check..."); + const step9 = await generate(systemPrompt, + STEP9_QA_CHECK.replace("{{ARTICLE}}", step8.text), + LLM_REFINE + ); + stepsCompleted = 9; + + // ═══ STEP 10: Quality Score ═══ + console.log(" Step 10/10: Quality Score..."); + let autoQaScore: Record | null = null; + try { + const step10 = await generate(systemPrompt, + STEP10_QUALITY_SCORE.replace("{{ARTICLE}}", step9.text), + { temperature: 0.2, maxTokens: 1024, timeoutMs: 120000 } + ); + // Try to parse JSON score + const jsonMatch = step10.text.match(/\{[\s\S]*"scores"[\s\S]*\}/); + if (jsonMatch) { + autoQaScore = JSON.parse(jsonMatch[0]); + console.log(` Auto QA Score: ${(autoQaScore as any)?.overall || "?"}/10`); } + } catch { + console.log(" Quality scoring skipped (parse error)"); } + stepsCompleted = 10; + // Final article + const draftContent = `# ${title}\n\n${step9.text}`; const wordCount = draftContent.split(/\s+/).length; const finalIssues = validateArticle(draftContent); // Update the draft in DB await pool.query( `UPDATE blog_drafts - SET draft_content = $1, word_count = $2, generated_by = 'tip-blog-engine-llm', - outline = $3, status = 'draft', updated_at = NOW() - WHERE id = $4::uuid`, + SET draft_content = $1, word_count = $2, + generated_by = 'fo-blog-engine-v3', + pipeline_version = 'v3-flexoptix-style', + pipeline_steps_completed = $3, + auto_qa_score = $4, + outline = $5, + status = 'draft', + updated_at = NOW() + WHERE id = $6::uuid`, [ draftContent, wordCount, - JSON.stringify({ generation_method: "llm", quality_issues: finalIssues }), + stepsCompleted, + autoQaScore ? JSON.stringify(autoQaScore) : null, + JSON.stringify({ + generation_method: "fo-pipeline-v3", + steps_completed: stepsCompleted, + blog_type: selectedTopic, + quality_issues: finalIssues, + feedback_entries_used: feedbackContext ? feedbackContext.split("\n").length : 0, + }), draftId, ], ); - console.log(`Blog LLM: Draft ${draftId} updated — ${wordCount} words, LLM-generated`); + // Auto-submit QA score as self-feedback + if (autoQaScore && (autoQaScore as any).scores) { + const s = (autoQaScore as any).scores; + await pool.query( + `INSERT INTO blog_feedback (blog_id, score_overall, score_technical_depth, score_real_world, + score_clarity, score_originality, score_engineer_voice, score_decision_value, + score_failure_scenarios, score_opinion_strength, reviewer, blog_type, blog_topic, improvements) + VALUES ($1::uuid, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'llm_self', $11, $12, $13)`, + [draftId, (autoQaScore as any).overall || 5, + s.technical_depth, s.real_world_relevance, s.clarity, s.originality, + s.engineer_voice, s.decision_value, s.failure_scenarios, s.opinion_strength, + selectedTopic, title, + (autoQaScore as any).improvements ? JSON.stringify((autoQaScore as any).improvements) : null] + ).catch(() => {}); + } + + console.log(`Blog FO Pipeline: ${draftId} complete — ${wordCount} words, ${stepsCompleted}/10 steps, QA: ${(autoQaScore as any)?.overall || "N/A"}/10`); } catch (llmErr) { - console.warn(`Blog LLM pipeline failed for ${draftId}: ${(llmErr as Error).message}`); - // Draft stays as template-fallback, no update needed + console.warn(`Blog FO Pipeline failed at step ${stepsCompleted + 1}/10 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', + outline = $2, updated_at = NOW() WHERE id = $3::uuid`, + [stepsCompleted, JSON.stringify({ error: (llmErr as Error).message, steps_completed: stepsCompleted }), draftId] + ).catch(() => {}); } }