Files
polit-scraper/analyzer.py

195 lines
6.6 KiB
Python

import sqlite3
import json
import os
import subprocess
import time
import sys
import logging
import re
import requests
from datetime import datetime
from dotenv import load_dotenv
# --- KONFIGURATION & LOGGING ---
load_dotenv()
BASE_DIR = os.path.dirname(__file__)
DEBUG_LOG = os.getenv("DEBUG_LOG", "false").lower() == "true"
LOG_LEVEL = logging.DEBUG if DEBUG_LOG else logging.INFO
MY_PID = os.getpid()
def sende_admin(titel, nachricht):
admin_url = f"{os.getenv('NTFY_BASE')}/polit-scraper-admin"
try:
requests.post(
admin_url,
data=nachricht.encode("utf-8"),
headers={"Title": titel.encode("utf-8"), "Priority": "4"},
auth=(os.getenv("ADMIN_USER"), os.getenv("ADMIN_PW")),
timeout=5
)
except: pass
logging.basicConfig(
level=LOG_LEVEL,
format='%(asctime)s [%(levelname)s] [%(process)d] %(message)s',
handlers=[
logging.FileHandler(os.path.join(BASE_DIR, "analyzer.log"), encoding='utf-8'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(f"analyzer_{MY_PID}")
GEMINI_MODEL = "gemini-3.5-flash"
GEMINI_EFFORT = os.getenv("GEMINI_EFFORT", "medium")
DB_PATH = os.path.join(BASE_DIR, "polit_scraper.db")
INSTRUCTION_PATH = os.path.join(BASE_DIR, "gemini_instructions.md")
def extract_json(text):
start = text.find("{")
ende = text.rfind("}") + 1
if start != -1 and ende > start:
json_str = text[start:ende]
json_str = re.sub(r'```json\s*', '', json_str)
json_str = re.sub(r'\s*```', '', json_str)
return json_str.strip()
return None
def analysiere_mit_gemini(text):
sicherer_text = text[:8000]
# Da agy (Antigravity CLI) keine GEMINI_SYSTEM_MD Umgebungsvariable mehr unterstützt,
# lesen wir die System-Instruktionen direkt aus der Datei und betten sie in den Prompt ein.
try:
with open(INSTRUCTION_PATH, "r", encoding="utf-8") as f:
system_instructions = f.read()
except Exception as e:
logger.error(f"Fehler beim Lesen der System-Instruktionen: {e}")
system_instructions = ""
anweisung = (
f"SYSTEM-INSTRUKTIONEN:\n{system_instructions}\n\n"
f"TEXT ZUR ANALYSE:\n{sicherer_text}\n\n"
f"Führe nun die Relevanz-Berechnung und JSON-Ausgabe gemäß der obigen SYSTEM-INSTRUKTIONEN durch."
)
env = os.environ.copy()
env["NODE_ENV"] = "production"
env["CI"] = "true"
env["FORCE_COLOR"] = "0"
# Migration zu agy (Antigravity CLI)
befehl = ["agy", "--model", GEMINI_MODEL, "--effort", GEMINI_EFFORT, "-p", anweisung]
try:
prozess = subprocess.run(
befehl,
capture_output=True,
text=True,
encoding="utf-8",
timeout=300,
env=env,
stdin=subprocess.DEVNULL
)
stdout = prozess.stdout.strip()
stderr = prozess.stderr.strip()
if DEBUG_LOG and "--test" not in sys.argv:
logger.debug(f"DEBUG_STDOUT: {stdout}")
if stderr: logger.debug(f"DEBUG_STDERR: {stderr}")
if "Error: setRawMode EIO" in stderr:
logger.error(f"TTY_ERROR detected in stderr: {stderr}")
return "TTY_ERROR"
if "exhausted your capacity" in stderr or "exhausted your capacity" in stdout:
return "RATE_LIMIT"
json_str = extract_json(stdout)
if json_str:
return json_str
logger.error(f"PARSING_FAILED. Roh-Output war: {stdout}\nSTDERR: {stderr}")
return None
except Exception as e:
logger.error(f"SUBPROCESS_CRASH: {e}")
return None
def test_mode():
print("\n=== ANALYZER TEST-MODUS ===")
print("Gib den Text ein, den du analysieren möchtest.")
print("(Beende die Eingabe mit Strg+D auf Linux/Mac oder Strg+Z auf Windows)")
print("--------------------------------------------------")
try:
input_text = sys.stdin.read().strip()
except EOFError:
input_text = ""
if not input_text:
print("\nAbgebrochen: Kein Text eingegeben.")
return
print("\n[INFO] Starte Analyse mit Gemini...")
ergebnis = analysiere_mit_gemini(input_text)
print("\n=== RESULTAT ===")
if ergebnis == "RATE_LIMIT":
print("FEHLER: Rate-Limit erreicht.")
elif ergebnis == "TTY_ERROR":
print("FEHLER: TTY/Terminal-Fehler.")
elif ergebnis:
print(ergebnis)
else:
print("FEHLER: Analyse schlug fehl oder kein JSON gefunden.")
print("================\n")
def main():
if "--test" in sys.argv:
test_mode()
return
logger.info(f"Worker {MY_PID} bereit.")
while True:
try:
conn = sqlite3.connect(DB_PATH, timeout=60)
cursor = conn.cursor()
cursor.execute("""
UPDATE analysis_queue
SET status='PROCESSING', worker_id=?, last_update=CURRENT_TIMESTAMP
WHERE id=(SELECT id FROM analysis_queue WHERE status='PENDING' ORDER BY id ASC LIMIT 1)
""", (MY_PID,))
conn.commit()
cursor.execute("SELECT id, url, raw_text FROM analysis_queue WHERE status='PROCESSING' AND worker_id=?", (MY_PID,))
job = cursor.fetchone()
if job:
job_id, url, text = job
logger.info(f"Analysiere Job {job_id}: {url}")
ergebnis = analysiere_mit_gemini(text)
if ergebnis == "RATE_LIMIT" or ergebnis == "TTY_ERROR":
logger.warning(f"Job {job_id} ({url}) returned {ergebnis}. Re-queueing and waiting.")
cursor.execute("UPDATE analysis_queue SET status='PENDING', worker_id=NULL WHERE id=?", (job_id,))
conn.commit()
time.sleep(60)
elif ergebnis:
cursor.execute("UPDATE analysis_queue SET status='ANALYZED', result_json=?, worker_id=NULL WHERE id=?", (ergebnis, job_id))
logger.info(f"Job {job_id} ANALYZED.")
conn.commit()
else:
cursor.execute("UPDATE analysis_queue SET status='FAILED', worker_id=NULL WHERE id=?", (job_id,))
logger.error(f"Job {job_id} FAILED (Parser returned None).")
conn.commit()
conn.close()
time.sleep(1)
except sqlite3.OperationalError:
time.sleep(5)
except Exception as e:
logger.error(f"Main Loop Fehler: {e}")
time.sleep(10)
if __name__ == "__main__": main()