LLM-Moderation mit DeepSeek V4 Flash + Admin-Kommentarverwaltung + BSN Assistent Umbenennung
- moderate_comment(): LLM-Filter prüft jeden neuen Kommentar auf deutsches Recht & Toxizität - /api/comment: Moderation vor Benachrichtigung, geblockte Kommentare stillschweigend verworfen - /admin/comments: Neue Admin-Ansicht für alle Kommentare mit Sperren/Freischalten - /admin/comment/<id>/<action>: API zum Blocken/Entsperren - comments.status + moderation_reason Felder in DB - 'Chat-Assistent' → 'BSN Assistent' überall umbenannt - System-Prompt: Beitrags-Links im Chat mit echten /beitrag/ID Verweisen
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
BSN Chatbot — Multi-Channel Intake System
|
BSN Assistent — Multi-Channel Intake System
|
||||||
==========================================
|
==========================================
|
||||||
WhatsApp webhook receiver + admin dashboard + knowledge query API.
|
WhatsApp webhook receiver + admin dashboard + knowledge query API.
|
||||||
|
|
||||||
@@ -147,6 +147,67 @@ def get_db() -> sqlite3.Connection:
|
|||||||
return g.db
|
return g.db
|
||||||
|
|
||||||
|
|
||||||
|
def moderate_comment(content: str) -> tuple[str, str | None]:
|
||||||
|
"""Run LLM moderation on a comment. Returns (status, reason)."""
|
||||||
|
import yaml as _yaml
|
||||||
|
import openai
|
||||||
|
config_path = os.path.expanduser("~/.hermes/config.yaml")
|
||||||
|
try:
|
||||||
|
with open(config_path) as f:
|
||||||
|
cfg = _yaml.safe_load(f)
|
||||||
|
api_key = cfg["providers"]["datenhimmel"]["api_key"]
|
||||||
|
base_url = cfg["providers"]["datenhimmel"]["base_url"]
|
||||||
|
except Exception:
|
||||||
|
print("[moderate] Cannot read API config", file=sys.stderr)
|
||||||
|
return "published", None
|
||||||
|
|
||||||
|
client = openai.OpenAI(base_url=base_url, api_key=api_key)
|
||||||
|
prompt = f"""Prüfe folgenden Kommentar auf einer Brettspiel-Community-Seite auf Verstöße gegen deutsches Recht und unsere Community-Regeln.
|
||||||
|
|
||||||
|
KOMMENTAR:
|
||||||
|
\"{content}\"
|
||||||
|
|
||||||
|
PRÜFE AUF:
|
||||||
|
1. Beleidigungen, persönliche Angriffe, Hassrede
|
||||||
|
2. Volksverhetzung, extremistische Inhalte
|
||||||
|
3. Gewaltandrohungen, Bedrohungen
|
||||||
|
4. Diskriminierung (Rasse, Religion, Geschlecht, sexuelle Orientierung, Behinderung)
|
||||||
|
5. Schwere Obszönitäten oder pornografische Inhalte
|
||||||
|
6. Illegale Inhalte nach deutschem Strafrecht
|
||||||
|
7. Spam oder offensichtlicher Vandalismus
|
||||||
|
|
||||||
|
Normale Kritik an Spielen, kontroverse Meinungen zu Brettspielen oder lebhafte Diskussionen sind ERLAUBT.
|
||||||
|
Nur klare Rechtsverstöße oder extreme Toxizität sollen geblockt werden.
|
||||||
|
|
||||||
|
Antworte NUR mit einem JSON-Objekt:
|
||||||
|
{{"status": "published", "reason": null}} — wenn der Kommentar in Ordnung ist
|
||||||
|
{{"status": "blocked", "reason": "Kurze Begründung auf Deutsch"}} — wenn der Kommentar gegen Regeln verstößt"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = client.chat.completions.create(
|
||||||
|
model="deepseek-v4-flash:cloud",
|
||||||
|
messages=[{"role": "user", "content": prompt}],
|
||||||
|
max_tokens=150,
|
||||||
|
temperature=0.0,
|
||||||
|
)
|
||||||
|
raw = r.choices[0].message.content.strip()
|
||||||
|
# Extract JSON from potential markdown fences
|
||||||
|
if "```" in raw:
|
||||||
|
raw = raw.split("```")[1]
|
||||||
|
if raw.startswith("json"):
|
||||||
|
raw = raw[4:]
|
||||||
|
data = json.loads(raw.strip())
|
||||||
|
status = data.get("status", "published")
|
||||||
|
reason = data.get("reason") or None
|
||||||
|
if status not in ("published", "blocked"):
|
||||||
|
status = "published"
|
||||||
|
print(f"[moderate] status={status} reason={reason}", file=sys.stderr)
|
||||||
|
return status, reason
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[moderate] error: {e}", file=sys.stderr)
|
||||||
|
return "published", None # Fail open — don't block on API errors
|
||||||
|
|
||||||
|
|
||||||
@app.teardown_appcontext
|
@app.teardown_appcontext
|
||||||
def close_db(_exception=None):
|
def close_db(_exception=None):
|
||||||
db = g.pop("db", None)
|
db = g.pop("db", None)
|
||||||
@@ -347,7 +408,7 @@ def auto_process_bg(db_path: str, submission_id: int):
|
|||||||
return
|
return
|
||||||
|
|
||||||
prompt = (
|
prompt = (
|
||||||
"Du bist Redaktionsassistent für brettspiel-news.de. Analysiere folgende Community-Nachricht "
|
"Du bist BSN Assistent für brettspiel-news.de. Analysiere folgende Community-Nachricht "
|
||||||
"und antworte NUR mit diesem JSON-Format, kein weiterer Text:\n"
|
"und antworte NUR mit diesem JSON-Format, kein weiterer Text:\n"
|
||||||
'{"summary": "Kurze Zusammenfassung in 1-2 Sätzen, max 250 Zeichen, journalistisch, Präsens", '
|
'{"summary": "Kurze Zusammenfassung in 1-2 Sätzen, max 250 Zeichen, journalistisch, Präsens", '
|
||||||
'"halle": "Halle und Stand im Format z.B. H3 B123, oder null wenn nichts erkannt", '
|
'"halle": "Halle und Stand im Format z.B. H3 B123, oder null wenn nichts erkannt", '
|
||||||
@@ -1025,6 +1086,110 @@ function toggleTheme(){{document.body.classList.toggle('light');localStorage.set
|
|||||||
</html>""".replace("{LOGO_B64}", LOGO_B64)
|
</html>""".replace("{LOGO_B64}", LOGO_B64)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/admin/comments")
|
||||||
|
def admin_comments():
|
||||||
|
"""Admin comment moderation — view all comments, filter by status."""
|
||||||
|
db = get_db()
|
||||||
|
status_filter = request.args.get("status", "")
|
||||||
|
query = """SELECT c.*, s.spiel_titel, s.sender_name AS sub_author
|
||||||
|
FROM comments c
|
||||||
|
LEFT JOIN submissions s ON c.submission_id = s.id
|
||||||
|
WHERE 1=1"""
|
||||||
|
params: list = []
|
||||||
|
if status_filter:
|
||||||
|
query += " AND c.status = ?"
|
||||||
|
params.append(status_filter)
|
||||||
|
query += " ORDER BY c.created_at DESC LIMIT 300"
|
||||||
|
rows = db.execute(query, params).fetchall()
|
||||||
|
|
||||||
|
status_counts = {
|
||||||
|
"all": db.execute("SELECT COUNT(*) FROM comments").fetchone()[0],
|
||||||
|
"published": db.execute("SELECT COUNT(*) FROM comments WHERE status='published'").fetchone()[0],
|
||||||
|
"blocked": db.execute("SELECT COUNT(*) FROM comments WHERE status='blocked'").fetchone()[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
tabs_html = '<div class="comment-tabs" style="display:flex;gap:0.5rem;margin-bottom:1rem;flex-wrap:wrap;">'
|
||||||
|
for key, label in [("", "Alle"), ("published", "Freigegeben"), ("blocked", "Gesperrt")]:
|
||||||
|
active = " style='background:#20228a;color:#fff;'" if status_filter == key else ""
|
||||||
|
count = status_counts.get(key or "all", 0)
|
||||||
|
tabs_html += f'<a href="/admin/comments?status={key}"{active} class="comment-tab-btn" style="padding:0.4rem 0.9rem;border:1.5px solid #ddd;border-radius:8px;text-decoration:none;font-size:0.8rem;color:#333;">{label} ({count})</a>'
|
||||||
|
tabs_html += '</div>'
|
||||||
|
|
||||||
|
comment_rows = ""
|
||||||
|
for c in rows:
|
||||||
|
c_content = (c["content"] or "")[:150]
|
||||||
|
c_author = (c["author_name"] or "Anonym")
|
||||||
|
c_date = c["created_at"] or ""
|
||||||
|
sub_label = c["spiel_titel"] or c["sub_author"] or f"#{c['submission_id']}"
|
||||||
|
status_badge = '<span style="color:#2e7d32;font-weight:600;">✅ Frei</span>' if c["status"] == "published" else '<span style="color:#c62828;font-weight:600;">🚫 Gesperrt</span>'
|
||||||
|
reason_html = f'<br><small style="color:#c62828;">Grund: {c["moderation_reason"]}</small>' if (c["moderation_reason"] if c["moderation_reason"] else None) else ""
|
||||||
|
action = f'<a href="#" onclick="toggleComment({c["id"]},\'unblock\')" style="color:#2e7d32;">Freischalten</a>' if c["status"] == "blocked" else f'<a href="#" onclick="toggleComment({c["id"]},\'block\')" style="color:#c62828;">Sperren</a>'
|
||||||
|
comment_rows += f"""<tr>
|
||||||
|
<td>{c['id']}</td>
|
||||||
|
<td><a href="/beitrag/{c['submission_id']}" target="_blank" style="color:#20228a;">{sub_label}</a></td>
|
||||||
|
<td>{c_author}</td>
|
||||||
|
<td style="max-width:300px;overflow-wrap:break-word;">{c_content}{reason_html}</td>
|
||||||
|
<td>{status_badge}</td>
|
||||||
|
<td style="font-size:0.7rem;color:#999;">{c_date}</td>
|
||||||
|
<td>{action}</td>
|
||||||
|
</tr>"""
|
||||||
|
|
||||||
|
return f"""<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>Kommentar-Moderation — BSN Admin</title>
|
||||||
|
<style>
|
||||||
|
* {{margin:0;padding:0;box-sizing:border-box;}}
|
||||||
|
body {{font-family:'Inter',sans-serif;background:#f5f3ee;color:#1a1a2e;padding:1rem;}}
|
||||||
|
h1 {{color:#20228a;margin-bottom:0.5rem;font-size:1.3rem;}}
|
||||||
|
table {{width:100%;border-collapse:collapse;font-size:0.85rem;}}
|
||||||
|
th {{background:#20228a;color:#fff;padding:0.6rem 0.5rem;text-align:left;font-size:0.75rem;}}
|
||||||
|
td {{padding:0.5rem;border-bottom:1px solid #eee;vertical-align:top;}}
|
||||||
|
tr:hover {{background:#f8f6f0;}}
|
||||||
|
.back-link {{display:inline-block;color:#20228a;text-decoration:none;font-weight:600;margin-bottom:1rem;}}
|
||||||
|
.admin-header {{display:flex;align-items:center;gap:1rem;margin-bottom:1.5rem;}}
|
||||||
|
.admin-header img {{height:34px;width:auto;}}
|
||||||
|
@media(max-width:700px){{table,thead,tbody,th,td,tr{{display:block;}}th{{display:none;}}tr{{margin-bottom:1rem;border:1px solid #eee;border-radius:8px;padding:0.5rem;}}td{{border:none;padding:0.2rem 0;}}}}
|
||||||
|
</style></head>
|
||||||
|
<body>
|
||||||
|
<div class="admin-header">
|
||||||
|
<img src="/static/logo.png" alt="BSN">
|
||||||
|
<div><h1>💬 Kommentar-Moderation</h1><a href="/admin" class="back-link">← Zurück zum Admin</a></div>
|
||||||
|
</div>
|
||||||
|
{tabs_html}
|
||||||
|
<script>
|
||||||
|
function toggleComment(id, action) {{
|
||||||
|
if (!confirm('Kommentar #' + id + ' ' + (action === 'block' ? 'sperren' : 'freischalten') + '?')) return;
|
||||||
|
fetch('/admin/comment/' + id + '/' + action, {{method:'POST'}})
|
||||||
|
.then(function(r){{return r.json();}})
|
||||||
|
.then(function(d){{if(d.ok)location.reload();else alert(d.error);}});
|
||||||
|
}}
|
||||||
|
</script>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>ID</th><th>Beitrag</th><th>Autor</th><th>Inhalt</th><th>Status</th><th>Datum</th><th>Aktion</th></tr></thead>
|
||||||
|
<tbody>{comment_rows}</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/admin/comment/<int:comment_id>/<action>", methods=["POST"])
|
||||||
|
def admin_comment_action(comment_id: int, action: str):
|
||||||
|
"""Block or unblock a comment."""
|
||||||
|
if action not in ("block", "unblock"):
|
||||||
|
return jsonify({"error": "Ungültige Aktion"}), 400
|
||||||
|
db = get_db()
|
||||||
|
row = db.execute("SELECT id, status FROM comments WHERE id = ?", (comment_id,)).fetchone()
|
||||||
|
if not row:
|
||||||
|
return jsonify({"error": "Kommentar nicht gefunden"}), 404
|
||||||
|
new_status = "blocked" if action == "block" else "published"
|
||||||
|
db.execute("UPDATE comments SET status = ?, moderation_reason = ? WHERE id = ?",
|
||||||
|
(new_status, "Admin-Manuell" if action == "block" else None, comment_id))
|
||||||
|
db.commit()
|
||||||
|
return jsonify({"ok": True, "status": new_status})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/test")
|
@app.route("/test")
|
||||||
def test_page():
|
def test_page():
|
||||||
"""Page with a curl snippet for simulated testing."""
|
"""Page with a curl snippet for simulated testing."""
|
||||||
@@ -1576,7 +1741,7 @@ def generate_llm_summary(sub_id: int):
|
|||||||
return jsonify({"error": "API-Key nicht konfiguriert"}), 500
|
return jsonify({"error": "API-Key nicht konfiguriert"}), 500
|
||||||
|
|
||||||
prompt = (
|
prompt = (
|
||||||
"Du bist Redaktionsassistent für brettspiel-news.de. "
|
"Du bist BSN Assistent für brettspiel-news.de. "
|
||||||
"Fasse folgende Community-Nachricht in 2-3 Sätzen zusammen. "
|
"Fasse folgende Community-Nachricht in 2-3 Sätzen zusammen. "
|
||||||
"Schreibe im Präsens, journalistisch, ohne Floskeln. "
|
"Schreibe im Präsens, journalistisch, ohne Floskeln. "
|
||||||
"Kein 'Der Nutzer schreibt...' oder 'In dieser Nachricht...'. "
|
"Kein 'Der Nutzer schreibt...' oder 'In dieser Nachricht...'. "
|
||||||
@@ -1651,7 +1816,7 @@ def public_frontend():
|
|||||||
cat_filter = request.args.get("cat", "").strip()
|
cat_filter = request.args.get("cat", "").strip()
|
||||||
sort = request.args.get("sort", "neu").strip()
|
sort = request.args.get("sort", "neu").strip()
|
||||||
|
|
||||||
query = ("SELECT s.*, (SELECT COUNT(*) FROM comments c WHERE c.submission_id = s.id) AS comment_count "
|
query = ("SELECT s.*, (SELECT COUNT(*) FROM comments c WHERE c.submission_id = s.id AND c.status='published') AS comment_count "
|
||||||
"FROM submissions s WHERE s.status='published' AND s.deleted_at IS NULL")
|
"FROM submissions s WHERE s.status='published' AND s.deleted_at IS NULL")
|
||||||
params = []
|
params = []
|
||||||
if halle_filter:
|
if halle_filter:
|
||||||
@@ -2179,7 +2344,7 @@ def public_frontend():
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- ── Chat Bubble ──────────────────────────────────────────── -->
|
<!-- ── Chat Bubble ──────────────────────────────────────────── -->
|
||||||
<button class="chat-bubble" id="chatBubble" title="BSN Community Assistant" aria-label="Chat öffnen">
|
<button class="chat-bubble" id="chatBubble" title="BSN Assistent" aria-label="Chat öffnen">
|
||||||
💬<span class="bubble-dot"></span>
|
💬<span class="bubble-dot"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -2197,7 +2362,7 @@ def public_frontend():
|
|||||||
</div>
|
</div>
|
||||||
<div class="chat-body" id="chatBody">
|
<div class="chat-body" id="chatBody">
|
||||||
<div style="display:flex;align-items:flex-end;gap:0.3rem;align-self:flex-start;">
|
<div style="display:flex;align-items:flex-end;gap:0.3rem;align-self:flex-start;">
|
||||||
<div class="chat-msg assistant">Hey! 👋 Ich bin der BSN Community Assistant. Frag mich nach Spielen, Ständen oder was auf der SPIEL Essen los ist!</div>
|
<div class="chat-msg assistant">Hey! 👋 Ich bin der BSN Assistent. Frag mich nach Spielen, Ständen oder was auf der SPIEL Essen los ist!</div>
|
||||||
<button id="greetingSpeakBtn" title="Vorlesen" style="background:rgba(32,34,138,0.08);border:1px solid rgba(32,34,138,0.2);border-radius:8px;cursor:pointer;font-size:0.9rem;padding:0.15rem 0.4rem;transition:all 0.15s;flex-shrink:0;" onmouseenter="this.style.background='rgba(32,34,138,0.18)';this.style.borderColor='rgba(32,34,138,0.4)'" onmouseleave="this.style.background='rgba(32,34,138,0.08)';this.style.borderColor='rgba(32,34,138,0.2)'">🔊</button>
|
<button id="greetingSpeakBtn" title="Vorlesen" style="background:rgba(32,34,138,0.08);border:1px solid rgba(32,34,138,0.2);border-radius:8px;cursor:pointer;font-size:0.9rem;padding:0.15rem 0.4rem;transition:all 0.15s;flex-shrink:0;" onmouseenter="this.style.background='rgba(32,34,138,0.18)';this.style.borderColor='rgba(32,34,138,0.4)'" onmouseleave="this.style.background='rgba(32,34,138,0.08)';this.style.borderColor='rgba(32,34,138,0.2)'">🔊</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2486,7 +2651,7 @@ def public_frontend():
|
|||||||
const greetBtn = document.getElementById('greetingSpeakBtn');
|
const greetBtn = document.getElementById('greetingSpeakBtn');
|
||||||
if (greetBtn) {
|
if (greetBtn) {
|
||||||
greetBtn.addEventListener('click', () => {
|
greetBtn.addEventListener('click', () => {
|
||||||
speak('Hey! Ich bin der BSN Community Assistant. Frag mich nach Spielen, Ständen oder was auf der SPIEL Essen los ist!');
|
speak('Hey! Ich bin der BSN Assistent. Frag mich nach Spielen, Ständen oder was auf der SPIEL Essen los ist!');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2587,7 +2752,7 @@ def public_frontend():
|
|||||||
if (history.length > 20) history = history.slice(-20);
|
if (history.length > 20) history = history.slice(-20);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
removeTyping();
|
removeTyping();
|
||||||
addMsg('assistant', 'Der Chat-Assistent ist gerade nicht erreichbar. Versuch es später nochmal.');
|
addMsg('assistant', 'Der BSN Assistent ist gerade nicht erreichbar. Versuch es später nochmal.');
|
||||||
}
|
}
|
||||||
sendBtn.disabled = false;
|
sendBtn.disabled = false;
|
||||||
input.focus();
|
input.focus();
|
||||||
@@ -2781,7 +2946,7 @@ def legal_datenschutz():
|
|||||||
<h2>5. Veröffentlichung von Beiträgen</h2>
|
<h2>5. Veröffentlichung von Beiträgen</h2>
|
||||||
<p>Bei Veröffentlichung werden sichtbar: Ihr Name (oder "Anonyme Quelle"), Beitragsinhalt, Medien, KI-Zusammenfassung, Halle/Kategorie. <strong>Ohne Namensangabe erfolgt die Veröffentlichung als "Anonyme Quelle".</strong></p>
|
<p>Bei Veröffentlichung werden sichtbar: Ihr Name (oder "Anonyme Quelle"), Beitragsinhalt, Medien, KI-Zusammenfassung, Halle/Kategorie. <strong>Ohne Namensangabe erfolgt die Veröffentlichung als "Anonyme Quelle".</strong></p>
|
||||||
|
|
||||||
<h2>6. Chat-Assistant (KI-Chat)</h2>
|
<h2>6. BSN Assistent (KI-Chat)</h2>
|
||||||
<p>Kein Zugriff auf nicht-veröffentlichte Daten. Keine Speicherung von Chat-Verläufen. Keine Verarbeitung personenbezogener Daten der Fragesteller.</p>
|
<p>Kein Zugriff auf nicht-veröffentlichte Daten. Keine Speicherung von Chat-Verläufen. Keine Verarbeitung personenbezogener Daten der Fragesteller.</p>
|
||||||
|
|
||||||
<h2>7. Cookies und Tracking</h2>
|
<h2>7. Cookies und Tracking</h2>
|
||||||
@@ -2834,7 +2999,7 @@ def legal_agb():
|
|||||||
<h2>8. Haftung</h2>
|
<h2>8. Haftung</h2>
|
||||||
<p>Unbeschränkt bei Vorsatz/grober Fahrlässigkeit. Bei leichter Fahrlässigkeit nur bei Kardinalpflichtverletzung, begrenzt auf vorhersehbaren Schaden. Keine Haftung für Nutzerinhalte. §§ 8–10 DDG: Keine Überwachungspflicht, aber Entfernung bei Kenntnis.</p>
|
<p>Unbeschränkt bei Vorsatz/grober Fahrlässigkeit. Bei leichter Fahrlässigkeit nur bei Kardinalpflichtverletzung, begrenzt auf vorhersehbaren Schaden. Keine Haftung für Nutzerinhalte. §§ 8–10 DDG: Keine Überwachungspflicht, aber Entfernung bei Kenntnis.</p>
|
||||||
|
|
||||||
<h2>9. KI-Chat-Assistant</h2>
|
<h2>9. KI-BSN Assistent</h2>
|
||||||
<p>Beantwortet Fragen auf Basis veröffentlichter Beiträge (DeepSeek V4 Flash). Antworten unverbindlich, können Fehler enthalten. Keine Speicherung von Chat-Verläufen.</p>
|
<p>Beantwortet Fragen auf Basis veröffentlichter Beiträge (DeepSeek V4 Flash). Antworten unverbindlich, können Fehler enthalten. Keine Speicherung von Chat-Verläufen.</p>
|
||||||
|
|
||||||
<h2>10. Änderungen der AGB</h2>
|
<h2>10. Änderungen der AGB</h2>
|
||||||
@@ -2932,7 +3097,7 @@ def api_chat():
|
|||||||
cat = r['category'] or ''
|
cat = r['category'] or ''
|
||||||
name = (r['sender_name'] or 'Anonym').strip()
|
name = (r['sender_name'] or 'Anonym').strip()
|
||||||
context_parts.append(
|
context_parts.append(
|
||||||
f"[{i}] {text}"
|
f"[Beitrag #{r['id']}] {text}"
|
||||||
+ (f" | 📍{halle}" if halle else '')
|
+ (f" | 📍{halle}" if halle else '')
|
||||||
+ (f" | 🎲{spiel}" if spiel else '')
|
+ (f" | 🎲{spiel}" if spiel else '')
|
||||||
+ (f" | #{cat}" if cat else '')
|
+ (f" | #{cat}" if cat else '')
|
||||||
@@ -2942,7 +3107,7 @@ def api_chat():
|
|||||||
|
|
||||||
# System prompt
|
# System prompt
|
||||||
system = (
|
system = (
|
||||||
"Du bist der BSN Community Assistant von brettspiel-news.de. "
|
"Du bist der BSN Assistent von boardgame.network / brettspiel-news.de. "
|
||||||
"Du hilfst Besuchern, Informationen über die Brettspiel-Community auf der SPIEL Essen zu finden.\n\n"
|
"Du hilfst Besuchern, Informationen über die Brettspiel-Community auf der SPIEL Essen zu finden.\n\n"
|
||||||
"WICHTIGE REGELN:\n"
|
"WICHTIGE REGELN:\n"
|
||||||
"- Beantworte Fragen NUR mit Informationen aus dem Kontext unten.\n"
|
"- Beantworte Fragen NUR mit Informationen aus dem Kontext unten.\n"
|
||||||
@@ -2951,7 +3116,10 @@ def api_chat():
|
|||||||
"- Antworte immer auf Deutsch, freundlich und hilfsbereit.\n"
|
"- Antworte immer auf Deutsch, freundlich und hilfsbereit.\n"
|
||||||
"- Halte Antworten kurz und präzise (2-4 Sätze).\n"
|
"- Halte Antworten kurz und präzise (2-4 Sätze).\n"
|
||||||
"- Bei Standortfragen nenne immer die Hallen-Nummer.\n"
|
"- Bei Standortfragen nenne immer die Hallen-Nummer.\n"
|
||||||
"- Der Kontext enthält nummerierte Beiträge [1], [2] usw. — du kannst darauf verweisen.\n\n"
|
"- Wenn du einen Beitrag erwähnst, verlinke ihn IMMER mit der Beitrags-ID aus dem Kontext.\n"
|
||||||
|
" Format: [Kurzbeschreibung](/beitrag/ID)\n"
|
||||||
|
" Beispiel: 'Schau dir [diesen Erfahrungsbericht](/beitrag/53) an!'\n"
|
||||||
|
" Nutze die Nummer aus 'Beitrag #ID' im Kontext als Linkziel.\n\n"
|
||||||
f"KONTEXT (veröffentlichte Community-Beiträge):\n{context[:8000]}"
|
f"KONTEXT (veröffentlichte Community-Beiträge):\n{context[:8000]}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2966,7 +3134,7 @@ def api_chat():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if not api_key:
|
if not api_key:
|
||||||
return jsonify({'reply': 'Chat-Assistent ist aktuell nicht verfügbar (API-Key fehlt).'})
|
return jsonify({'reply': 'BSN Assistent ist aktuell nicht verfügbar (API-Key fehlt).'})
|
||||||
|
|
||||||
# Build messages with conversation history
|
# Build messages with conversation history
|
||||||
messages = [{"role": "system", "content": system}]
|
messages = [{"role": "system", "content": system}]
|
||||||
@@ -2985,10 +3153,10 @@ def api_chat():
|
|||||||
timeout=15,
|
timeout=15,
|
||||||
)
|
)
|
||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
return jsonify({'reply': 'Der Chat-Assistent antwortet gerade nicht. Bitte versuch es später nochmal.'})
|
return jsonify({'reply': 'Der BSN Assistent antwortet gerade nicht. Bitte versuch es später nochmal.'})
|
||||||
reply = r.json()["choices"][0]["message"]["content"].strip()
|
reply = r.json()["choices"][0]["message"]["content"].strip()
|
||||||
except Exception:
|
except Exception:
|
||||||
return jsonify({'reply': 'Der Chat-Assistent antwortet gerade nicht. Bitte versuch es später nochmal.'})
|
return jsonify({'reply': 'Der BSN Assistent antwortet gerade nicht. Bitte versuch es später nochmal.'})
|
||||||
|
|
||||||
return jsonify({'reply': reply})
|
return jsonify({'reply': reply})
|
||||||
|
|
||||||
@@ -3234,7 +3402,7 @@ def public_detail(sub_id: int):
|
|||||||
|
|
||||||
# Comments
|
# Comments
|
||||||
comment_rows = db.execute(
|
comment_rows = db.execute(
|
||||||
"SELECT id, author_name, content, created_at, parent_id FROM comments WHERE submission_id = ? ORDER BY created_at ASC",
|
"SELECT id, author_name, content, created_at, parent_id FROM comments WHERE submission_id = ? AND status='published' ORDER BY created_at ASC",
|
||||||
(sub_id,),
|
(sub_id,),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
||||||
@@ -3580,6 +3748,19 @@ def api_comment(sub_id: int):
|
|||||||
db.commit()
|
db.commit()
|
||||||
comment_id = db.execute("SELECT last_insert_rowid()").fetchone()[0]
|
comment_id = db.execute("SELECT last_insert_rowid()").fetchone()[0]
|
||||||
|
|
||||||
|
# LLM Moderation — run after insert, update status
|
||||||
|
mod_status, mod_reason = moderate_comment(content)
|
||||||
|
db.execute(
|
||||||
|
"UPDATE comments SET status=?, moderation_reason=? WHERE id=?",
|
||||||
|
(mod_status, mod_reason, comment_id),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# If blocked, skip notifications and return success silently
|
||||||
|
if mod_status == "blocked":
|
||||||
|
print(f"[moderate] Comment #{comment_id} BLOCKED: {mod_reason}", file=sys.stderr)
|
||||||
|
return jsonify({"ok": True, "comment_id": comment_id}), 201
|
||||||
|
|
||||||
# Build notification link
|
# Build notification link
|
||||||
base = request.host_url.rstrip("/")
|
base = request.host_url.rstrip("/")
|
||||||
link = f"{base}/beitrag/{sub_id}"
|
link = f"{base}/beitrag/{sub_id}"
|
||||||
|
|||||||
Reference in New Issue
Block a user