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(() => {}); } }