Files
BSN-Chatsystem/article_pipeline.py
T

223 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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. <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}."')
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()