diff --git a/article_pipeline.py b/article_pipeline.py new file mode 100644 index 0000000..f461a20 --- /dev/null +++ b/article_pipeline.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +""" +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, +der dann via delegate_task Research (Stage 1) und Write+QC (Stage 2+3) durchführt. +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 json +import os +import sys +import subprocess +import tempfile +from datetime import datetime + +WORKSPACE = "/home/hermes/workspace" +DOC_CACHE = os.path.expanduser("~/.hermes/cache/documents") + + +def ocr_pdf(pdf_path: str) -> str: + """PDF mit pymupdf OCRen und Text extrahieren.""" + import pymupdf + doc = pymupdf.open(pdf_path) + texts = [] + for page in doc: + texts.append(page.get_text()) + doc.close() + 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, + category_id: int, image_urls: list[str], + deadline: str | None, + pdf_texts: dict[str, str]) -> str: + """Direkten Agent-Prompt für den Artikel-Workflow bauen.""" + + now = datetime.now() + date_str = now.strftime("%d. %B %Y") + slug = topic.lower().replace(" ", "_").replace(":", "").replace("×", "x")[:40] + + lines = [] + lines.append(f"Führe den BSN-Artikel-Workflow für folgendes Thema aus:") + lines.append(f"**Thema:** {topic}") + lines.append(f"**Datum:** {date_str}") + lines.append(f"**Rubrik:** {rubric} (Joomla catid={category_id})") + if deadline: + lines.append(f"**Deadline:** {deadline}") + lines.append(f"**Dateiname:** artikel_{slug}_{now.strftime('%Y-%m-%d')}.html") + lines.append("") + + # PDF-OCR-Ergebnisse + if pdf_texts: + lines.append("## PDF-Quellen (OCR-extrahiert, bereits im Context)") + for path, text in pdf_texts.items(): + fname = os.path.basename(path) + truncated = text[:3000] + if len(text) > 3000: + truncated += f"\n... (gekürzt, Original: {len(text)} Zeichen)" + lines.append(f"\n### {fname}\n{truncated}\n") + lines.append("") + + # URL-Quellen + if sources: + lines.append("## URL-Quellen") + for s in sources: + lines.append(f"- {s}") + lines.append("") + + # STAGE 1: Research + lines.append("## STAGE 1: Research") + lines.append("") + lines.append("Nutze **delegate_task** um einen Research-Subagent zu starten:") + 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("- 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("") + 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("") + + # STAGE 2+3: Write+QC + lines.append("## STAGE 2+3: Write + QC") + lines.append("") + lines.append("Nutze **delegate_task** für einen Write+QC-Subagent:") + 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(f'- context: "HTML-Regeln: KEIN XML API v2, KEIN Betriebsgeheimnis, KEINE Emojis.

mit Datum+Lesezeit. Genau 1×


. Quellen als Quellen +
    mit .

    mit Quellen-Ratings. Datei endet mit . Rubrik: {rubric}."') + lines.append('- toolsets: ["web", "terminal", "file", "search", "skills"]') + lines.append("") + lines.append("**WICHTIG:** Warte bis Sub-Agent #2 fertig. Prüfe Exit-Code und Datei.") + lines.append("") + + # STAGE 4: Deliver + lines.append("## STAGE 4: Deliver") + lines.append("") + lines.append("1. Git-Commit:") + lines.append(f" ```bash") + 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" ```") + 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("3. Basti benachrichtigen per send_message (Telegram):") + lines.append(f" ✅ Artikel fertig: {topic}") + lines.append(f" 📄 /home/hermes/workspace/artikel_{slug}_{now.strftime('%Y-%m-%d')}.html") + lines.append(" 📊 verify_article.sh: 0 Fehler") + + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser( + description="BSN Article Pipeline — Stage 0 Prompt Builder" + ) + parser.add_argument("--topic", help="Artikel-Thema") + parser.add_argument("--sources", nargs="*", default=[], + help="URLs oder PDF-Pfade") + parser.add_argument("--rubric", default="Korrektur", + help="Rubrik-Name (default: Korrektur)") + parser.add_argument("--category-id", type=int, default=8, + help="Joomla Category-ID (default: 8)") + parser.add_argument("--image-urls", nargs="*", default=[], + help="Bild-URLs aus PM") + parser.add_argument("--deadline", default=None, + help="ISO-Timestamp für Deadline") + parser.add_argument("--batch", help="JSON-Datei mit Batch-Inputs") + + args = parser.parse_args() + + # Batch-Modus + if args.batch: + with open(args.batch) as f: + batch_inputs = json.load(f) + outputs = [] + for inp in batch_inputs: + prompt = build_prompt( + topic=inp.get("topic", ""), + sources=inp.get("sources", []), + rubric=inp.get("rubric", "Korrektur"), + category_id=inp.get("category_id", 8), + image_urls=inp.get("image_urls", []), + deadline=inp.get("deadline"), + pdf_texts={} + ) + outputs.append({ + "topic": inp.get("topic"), + "prompt": prompt + }) + print(json.dumps(outputs, indent=2, ensure_ascii=False)) + return + + # Einzel-Modus + if not args.topic: + print("ERROR: --topic erforderlich (oder --batch)", file=sys.stderr) + sys.exit(1) + + # PDFs OCRen + pdf_texts = {} + for src in args.sources: + if src.lower().endswith(".pdf"): + full_path = src + if not os.path.isabs(full_path): + full_path = os.path.join(DOC_CACHE, src) + if os.path.exists(full_path): + try: + pdf_texts[full_path] = ocr_pdf(full_path) + print(f"[Stage 0] PDF OCR: {full_path} " + f"({len(pdf_texts[full_path])} Zeichen)", file=sys.stderr) + except Exception as e: + print(f"[Stage 0] PDF-Fehler: {e}", file=sys.stderr) + + # Prompt bauen + prompt = build_prompt( + topic=args.topic, + sources=args.sources, + rubric=args.rubric, + category_id=args.category_id, + image_urls=args.image_urls, + deadline=args.deadline, + pdf_texts=pdf_texts, + ) + + print(prompt) + + +if __name__ == "__main__": + main()