fix: Pipeline — Dachzeile, Infobox, Read-More-Position, Quellen-Format v2
This commit is contained in:
+69
-45
@@ -5,30 +5,12 @@ article_pipeline.py — Stage 0: Input-Preparation für den BSN-Artikel-Workflow
|
|||||||
Liest Topic, PDFs, URLs und baut einen strukturierten Prompt für den Hermes-Agenten,
|
Liest Topic, PDFs, URLs und baut einen strukturierten Prompt für den Hermes-Agenten,
|
||||||
der dann via delegate_task Research (Stage 1) und Write+QC (Stage 2+3) durchführt.
|
der dann via delegate_task Research (Stage 1) und Write+QC (Stage 2+3) durchführt.
|
||||||
Stage 4 (Git, Verify) macht der Agent selbst.
|
Stage 4 (Git, Verify) macht der Agent selbst.
|
||||||
|
|
||||||
Usage:
|
|
||||||
# Thema + PDF
|
|
||||||
python3 article_pipeline.py --topic "Gamegenic × TCC" --pdf pm.pdf --rubric "Nachrichten"
|
|
||||||
|
|
||||||
# Thema + URL
|
|
||||||
python3 article_pipeline.py --topic "Neue Kennerspiel-Nominierungen" --url "https://..."
|
|
||||||
|
|
||||||
# Nur Thema (autonome Recherche)
|
|
||||||
python3 article_pipeline.py --topic "Spiel des Jahres 2026" --rubric "Nachrichten"
|
|
||||||
|
|
||||||
# Batch (JSON)
|
|
||||||
python3 article_pipeline.py --batch topics.json
|
|
||||||
|
|
||||||
Output: Strukturierter Prompt nach stdout (wird vom Hermes-Cronjob als Context injiziert)
|
|
||||||
Exit-Codes: 0 = Prompt bereit, 1 = Fehler in Stage 0
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
WORKSPACE = "/home/hermes/workspace"
|
WORKSPACE = "/home/hermes/workspace"
|
||||||
@@ -46,23 +28,6 @@ def ocr_pdf(pdf_path: str) -> str:
|
|||||||
return "\n\n".join(texts)
|
return "\n\n".join(texts)
|
||||||
|
|
||||||
|
|
||||||
def extract_url_text(url: str) -> str:
|
|
||||||
"""URL-Inhalt via 5-Stufen-Eskalation abrufen."""
|
|
||||||
import urllib.request
|
|
||||||
import urllib.error
|
|
||||||
|
|
||||||
# Stufe 1: curl direkt
|
|
||||||
try:
|
|
||||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
|
||||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
||||||
return resp.read().decode("utf-8", errors="ignore")[:5000]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Stufe 2-5 wäre hier — für Stage 0 reicht Stufe 1
|
|
||||||
return f"[URL nicht direkt abrufbar: {url}]"
|
|
||||||
|
|
||||||
|
|
||||||
def build_prompt(topic: str, sources: list[str], rubric: str,
|
def build_prompt(topic: str, sources: list[str], rubric: str,
|
||||||
category_id: int, image_urls: list[str],
|
category_id: int, image_urls: list[str],
|
||||||
deadline: str | None,
|
deadline: str | None,
|
||||||
@@ -72,7 +37,48 @@ def build_prompt(topic: str, sources: list[str], rubric: str,
|
|||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
date_str = now.strftime("%d. %B %Y")
|
date_str = now.strftime("%d. %B %Y")
|
||||||
slug = topic.lower().replace(" ", "_").replace(":", "").replace("×", "x")[:40]
|
slug = topic.lower().replace(" ", "_").replace(":", "").replace("×", "x")[:40]
|
||||||
|
date_slug = now.strftime("%Y-%m-%d")
|
||||||
|
filename = f"artikel_{slug}_{date_slug}.html"
|
||||||
|
|
||||||
|
# --- WRITE+QC CONTEXT (most complex part) ---
|
||||||
|
write_context = (
|
||||||
|
"STRUKTUR (exakt einhalten, nummeriert):\n"
|
||||||
|
"1. <h1>TITEL</h1>\n"
|
||||||
|
'2. DACHZEILE: <div style="font-size:1.1rem;color:#555;margin-bottom:1.5rem;'
|
||||||
|
'border-bottom:2px solid #e0e0e0;padding-bottom:1rem;">'
|
||||||
|
'Zusammenfassung in 1-2 Sätzen</div>\n'
|
||||||
|
'3. <p class="meta" style="font-size:0.85rem;color:#888;margin-bottom:2rem;">'
|
||||||
|
'DATUM · Lesezeit: ca. N Minuten</p>\n'
|
||||||
|
'4. Einleitungs-Absatz <p>\n'
|
||||||
|
'5. INFOBOX (PFLICHT bei Unternehmen/Produkt-News): '
|
||||||
|
'<div style="background:#f0f4f8;border:1px solid #d0d8e0;border-radius:6px;'
|
||||||
|
'padding:1.2rem 1.5rem;margin:2rem 0;">'
|
||||||
|
'<h3 style="margin-top:0;color:#20228A;font-family:Arial,sans-serif;font-size:1.15rem;">'
|
||||||
|
'NAME — Fakten</h3><table style="width:100%;border-collapse:collapse;">'
|
||||||
|
'<tr><td style="padding:0.3rem 0.5rem;font-weight:bold;color:#444;width:160px;">'
|
||||||
|
'Label</td><td style="padding:0.3rem 0.5rem;">Wert</td></tr></table></div>\n'
|
||||||
|
'6. ALLE Inhalts-Abschnitte (<h2> + <p>)\n'
|
||||||
|
'7. <hr id="system-readmore" /> <-- NACH allen Inhalten, VOR Quellen!\n'
|
||||||
|
'8. Quellen-Überschrift: <span style="font-size:1.17em;font-weight:bold;'
|
||||||
|
'display:block;margin:1em 0;"><strong>Quellen</strong></span>\n'
|
||||||
|
'9. JEDE Quelle als: <li><a href="URL" target="_blank" rel="noopener" '
|
||||||
|
'style="color:#0075FF;">Name</a> — Beschreibung</li>\n'
|
||||||
|
'10. <p class="qc"><!-- QC: Quelle1=AAA|URL1, Quelle2=AA|URL2 --></p>\n'
|
||||||
|
'11. <p style="margin-top:2.5rem;padding-top:1rem;border-top:1px solid #ddd;'
|
||||||
|
'font-size:0.9rem;color:#777;"><em>Recherchestand: DATUM. '
|
||||||
|
'Basierend auf QUELLEN.</em></p>\n'
|
||||||
|
'\n'
|
||||||
|
'KRITISCHE REGELN (QC-Fail bei Verstoß):\n'
|
||||||
|
'- Read-More NACH allen Inhalten, VOR Quellen (NICHT nach Absatz 1!)\n'
|
||||||
|
'- JEDE Quelle als <a href> mit target="_blank" — KEINE unverlinkten Quellen!\n'
|
||||||
|
'- Dachzeile + Infobox sind PFLICHT\n'
|
||||||
|
'- Quellen-Überschrift: font-size:1.17em (NICHT 0.9rem)\n'
|
||||||
|
'- Kein "XML API v2", kein "Betriebsgeheimnis", keine Emojis\n'
|
||||||
|
'- Datei endet mit </html>\n'
|
||||||
|
f'- Rubrik: {rubric}'
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- BUILD OUTPUT ---
|
||||||
lines = []
|
lines = []
|
||||||
lines.append(f"Führe den BSN-Artikel-Workflow für folgendes Thema aus:")
|
lines.append(f"Führe den BSN-Artikel-Workflow für folgendes Thema aus:")
|
||||||
lines.append(f"**Thema:** {topic}")
|
lines.append(f"**Thema:** {topic}")
|
||||||
@@ -80,7 +86,7 @@ def build_prompt(topic: str, sources: list[str], rubric: str,
|
|||||||
lines.append(f"**Rubrik:** {rubric} (Joomla catid={category_id})")
|
lines.append(f"**Rubrik:** {rubric} (Joomla catid={category_id})")
|
||||||
if deadline:
|
if deadline:
|
||||||
lines.append(f"**Deadline:** {deadline}")
|
lines.append(f"**Deadline:** {deadline}")
|
||||||
lines.append(f"**Dateiname:** artikel_{slug}_{now.strftime('%Y-%m-%d')}.html")
|
lines.append(f"**Dateiname:** {filename}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# PDF-OCR-Ergebnisse
|
# PDF-OCR-Ergebnisse
|
||||||
@@ -106,11 +112,24 @@ def build_prompt(topic: str, sources: list[str], rubric: str,
|
|||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("Nutze **delegate_task** um einen Research-Subagent zu starten:")
|
lines.append("Nutze **delegate_task** um einen Research-Subagent zu starten:")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(f"- goal: \"Recherchiere zum Thema: {topic}. Sammle Fakten aus allen Quellen, bewerte sie (AAA/AA/A), ergänze BGG-Daten (Designer, Verlag, Jahr). Erstelle Research-Brief in /tmp/research_brief.md. Bei unvollständigen Kern-Fakten → Exit 1.\"")
|
lines.append(
|
||||||
lines.append("- context: Enthält PDF-Texte und URLs (siehe oben). Nutze 5-Stufen-Eskalation für unerreichbare URLs. web_search site:boardgamegeek.com für BGG-Daten.")
|
f'- goal: "Recherchiere zum Thema: {topic}. '
|
||||||
|
f'Sammle Fakten aus allen Quellen, bewerte sie (AAA/AA/A), '
|
||||||
|
f'ergänze BGG-Daten (Designer, Verlag, Jahr). '
|
||||||
|
f'Erstelle Research-Brief in /tmp/research_brief.md. '
|
||||||
|
f'Bei unvollständigen Kern-Fakten → Exit 1."'
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
'- context: "Enthält PDF-Texte und URLs (siehe oben). '
|
||||||
|
'Nutze 5-Stufen-Eskalation für unerreichbare URLs. '
|
||||||
|
'web_search site:boardgamegeek.com für BGG-Daten."'
|
||||||
|
)
|
||||||
lines.append('- toolsets: ["web", "terminal", "file", "search", "skills"]')
|
lines.append('- toolsets: ["web", "terminal", "file", "search", "skills"]')
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("**WICHTIG:** Warte bis Sub-Agent #1 fertig ist. Prüfe /tmp/research_brief.md. Bei Exit-Code 1 → MELDUNG an Basti, NICHT fortfahren.")
|
lines.append(
|
||||||
|
"**WICHTIG:** Warte bis Sub-Agent #1 fertig ist. "
|
||||||
|
"Prüfe /tmp/research_brief.md. Bei Exit-Code 1 → MELDUNG an Basti, NICHT fortfahren."
|
||||||
|
)
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# STAGE 2+3: Write+QC
|
# STAGE 2+3: Write+QC
|
||||||
@@ -118,8 +137,15 @@ def build_prompt(topic: str, sources: list[str], rubric: str,
|
|||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("Nutze **delegate_task** für einen Write+QC-Subagent:")
|
lines.append("Nutze **delegate_task** für einen Write+QC-Subagent:")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(f'- goal: "Schreibe HTML-Artikel zum Thema: {topic}. Nutze Research-Brief aus /tmp/research_brief.md. Datei: artikel_{slug}_{now.strftime("%Y-%m-%d")}.html in /home/hermes/workspace/. Danach: bash /home/hermes/workspace/verify_article.sh artikel_{slug}_{now.strftime("%Y-%m-%d")}.html. Bei Fehlern korrigieren, max 3 Versuche."')
|
lines.append(
|
||||||
lines.append(f'- context: "HTML-Regeln: KEIN XML API v2, KEIN Betriebsgeheimnis, KEINE Emojis. <p class=\\"meta\\"> mit Datum+Lesezeit. Genau 1× <hr id=\\"system-readmore\\" />. Quellen als <span><strong>Quellen</strong></span> + <ol> mit <a target=\\"_blank\\">. <p class=\\"qc\\"> mit Quellen-Ratings. Datei endet mit </html>. Rubrik: {rubric}."')
|
f'- goal: "Schreibe HTML-Artikel zum Thema: {topic}. '
|
||||||
|
f'Nutze Research-Brief aus /tmp/research_brief.md. '
|
||||||
|
f'Datei: /home/hermes/workspace/{filename}. '
|
||||||
|
f'Danach: bash /home/hermes/workspace/verify_article.sh '
|
||||||
|
f'/home/hermes/workspace/{filename}. '
|
||||||
|
f'Bei Fehlern korrigieren, max 3 Versuche."'
|
||||||
|
)
|
||||||
|
lines.append(f'- context: """\n{write_context}\n"""')
|
||||||
lines.append('- toolsets: ["web", "terminal", "file", "search", "skills"]')
|
lines.append('- toolsets: ["web", "terminal", "file", "search", "skills"]')
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("**WICHTIG:** Warte bis Sub-Agent #2 fertig. Prüfe Exit-Code und Datei.")
|
lines.append("**WICHTIG:** Warte bis Sub-Agent #2 fertig. Prüfe Exit-Code und Datei.")
|
||||||
@@ -129,15 +155,13 @@ def build_prompt(topic: str, sources: list[str], rubric: str,
|
|||||||
lines.append("## STAGE 4: Deliver")
|
lines.append("## STAGE 4: Deliver")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("1. Git-Commit:")
|
lines.append("1. Git-Commit:")
|
||||||
lines.append(f" ```bash")
|
lines.append(f" cd /home/hermes/workspace && git add {filename}")
|
||||||
lines.append(f" cd /home/hermes/workspace && git add artikel_{slug}_{now.strftime('%Y-%m-%d')}.html")
|
|
||||||
lines.append(f' git commit -m "artikel: {topic[:50]}"')
|
lines.append(f' git commit -m "artikel: {topic[:50]}"')
|
||||||
lines.append(f" ```")
|
|
||||||
lines.append("2. Prüfen ob Article-Server erreichbar:")
|
lines.append("2. Prüfen ob Article-Server erreichbar:")
|
||||||
lines.append(" curl -s 'http://185.162.249.159:8890/?key=br3ttsp1el-n3ws-2026' | head -3")
|
lines.append(" curl -s 'http://185.162.249.159:8890/?key=br3ttsp1el-n3ws-2026' | head -3")
|
||||||
lines.append("3. Basti benachrichtigen per send_message (Telegram):")
|
lines.append("3. Basti benachrichtigen per send_message (Telegram):")
|
||||||
lines.append(f" ✅ Artikel fertig: {topic}")
|
lines.append(f" ✅ Artikel fertig: {topic}")
|
||||||
lines.append(f" 📄 /home/hermes/workspace/artikel_{slug}_{now.strftime('%Y-%m-%d')}.html")
|
lines.append(f" 📄 /home/hermes/workspace/{filename}")
|
||||||
lines.append(" 📊 verify_article.sh: 0 Fehler")
|
lines.append(" 📊 verify_article.sh: 0 Fehler")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|||||||
Reference in New Issue
Block a user