fix: markdown→HTML rendering in engine + harden thinking fallback
- Engine now converts content_md to HTML at render time (was dumping raw markdown, causing articles to show literal #/**/- symbols and collapse into wall of text) - /api/publish accepts 'content' key and converts markdown→HTML for API consumers - Added md Jinja filter + md_to_html helper (markdown lib, extra+sane_lists) - orchestrator: log warning when falling back to 'thinking' field (CoT, not prose) - content_pipeline now generates formatted articles via LLM instead of raw scraped HTML
This commit is contained in:
@@ -205,9 +205,14 @@ def llm_chat(prompt: str, model: str = "qwen3.5:4b-mlx", host: str = OLLAMA_MACB
|
||||
result = r.json()
|
||||
if "message" in result:
|
||||
content = result["message"].get("content", "")
|
||||
# ornith puts output in 'thinking' when content is empty
|
||||
# ornith puts output in 'thinking' when content is empty.
|
||||
# WARNING: 'thinking' is chain-of-thought reasoning, NOT article text.
|
||||
# Only fall back to it for JSON/short tasks, never long-form prose.
|
||||
if not content:
|
||||
content = result["message"].get("thinking", "")
|
||||
if content:
|
||||
log.warning(f"LLM {model} returned empty content — fell back to 'thinking' field ({len(content)} chars). "
|
||||
f"Verify this is real output, not chain-of-thought.")
|
||||
if content:
|
||||
return content
|
||||
if "error" in result:
|
||||
|
||||
@@ -10,6 +10,24 @@ from pathlib import Path
|
||||
from datetime import datetime
|
||||
from flask import Flask, request, jsonify, render_template_string, g, abort, Response
|
||||
|
||||
try:
|
||||
import markdown as _md
|
||||
except ImportError:
|
||||
_md = None
|
||||
|
||||
|
||||
def md_to_html(text):
|
||||
"""Convert Markdown to HTML for article rendering."""
|
||||
if not text:
|
||||
return ""
|
||||
if _md is not None:
|
||||
return _md.markdown(text, extensions=["extra", "sane_lists"])
|
||||
# Minimal fallback (markdown lib not installed)
|
||||
import re as _re
|
||||
out = _re.sub(r"^#{1,6}\s+(.+)$", r"<h3>\1</h3>", text, flags=_re.M)
|
||||
out = _re.sub(r"^\*\*(.+?)\*\*$", r"<strong>\1</strong>", out, flags=_re.M)
|
||||
return "<p>" + out.replace("\n\n", "</p><p>").replace("\n", "<br>") + "</p>"
|
||||
|
||||
# ─── Config ────────────────────────────────────────────────────────
|
||||
VERTICAL = os.environ.get("PUBLISHER_VERTICAL", "guides")
|
||||
DOMAIN = f"{VERTICAL}.thetempleofdoom.com"
|
||||
@@ -520,8 +538,10 @@ def api_publish():
|
||||
|
||||
slug = data.get("slug", "")
|
||||
title = data.get("title", "")
|
||||
content_html = data.get("content_html", data.get("content_md", ""))
|
||||
content_md = data.get("content_md", "")
|
||||
content_md = data.get("content_md") or data.get("content") or ""
|
||||
content_html = data.get("content_html", "")
|
||||
if not content_html and content_md:
|
||||
content_html = md_to_html(content_md)
|
||||
excerpt = data.get("excerpt", data.get("seo_description", ""))
|
||||
seo_title = data.get("seo_title", title)
|
||||
seo_description = data.get("seo_description", "")
|
||||
@@ -1113,7 +1133,7 @@ ARTICLE_TEMPLATE = """<!DOCTYPE html>
|
||||
</div>
|
||||
|
||||
<div class="article-content">
|
||||
{{ article.content_html|safe }}
|
||||
{{ (article.content_md or article.content_html)|md|safe }}
|
||||
</div>
|
||||
|
||||
<footer class="article-footer">
|
||||
@@ -1392,6 +1412,11 @@ def from_json_filter(s):
|
||||
return []
|
||||
|
||||
|
||||
@app.template_filter("md")
|
||||
def md_filter(s):
|
||||
return md_to_html(s or "")
|
||||
|
||||
|
||||
# ─── Main ──────────────────────────────────────────────────────────
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
Reference in New Issue
Block a user