feat: Ausverkauft-Auto-Markierung + Chatbot-Integration

Webhook:
- mark_ausverkauft_in_neuheiten(): Cross-Reference-Funktion
  setzt Bit 0 in neuheiten.flags bei #out-Meldungen
- Aufruf sowohl bei neuen Submissions als auch Thread-Appends
- Gefixt: lokales 'import re' entfernt (überschattete globales re)

Chatbot /api/chat:
- Neuheiten-SELECT inkludiert flags-Spalte
- [Neuheit]-Einträge zeigen '🔴 AUSVERKAUFT' bei flags & 1
- [Ausverkauft-Meldung]-Suche in published Submissions
- System-Prompt um Ausverkauft-Regeln ergänzt

Flow: WhatsApp '#out Spielname' → Submission → neuheiten.flags
→ Chatbot warnt aktiv vor vergriffenen Spielen
This commit is contained in:
Hermes Agent
2026-06-18 15:11:38 +02:00
parent bdd7ebb006
commit f6581b7de5
+59 -2
View File
@@ -615,6 +615,24 @@ def _transcribe_video_bg(db_path: str, submission_id: int, video_path: str):
auto_process_bg(db_path, submission_id) auto_process_bg(db_path, submission_id)
def mark_ausverkauft_in_neuheiten(db, spiel_titel):
"""Cross-reference ausverkauft submission with neuheiten table.
Sets bit 0 of neuheiten.flags to mark the game as sold out.
Returns number of matched neuheiten rows."""
if not spiel_titel:
return 0
try:
# Use LIKE for fuzzy matching (handles minor spelling variations)
result = db.execute(
"UPDATE neuheiten SET flags = flags | 1 WHERE LOWER(titel) LIKE ?",
(f"%{spiel_titel.lower().strip()}%",),
)
db.commit()
return result.rowcount
except Exception:
return 0
# ── Routes ────────────────────────────────────────────────────────── # ── Routes ──────────────────────────────────────────────────────────
@app.route("/whatsapp/webhook", methods=["GET", "POST"]) @app.route("/whatsapp/webhook", methods=["GET", "POST"])
def whatsapp_webhook(): def whatsapp_webhook():
@@ -753,6 +771,9 @@ def whatsapp_webhook():
"UPDATE submissions SET category=?, spiel_titel=? WHERE id=?", "UPDATE submissions SET category=?, spiel_titel=? WHERE id=?",
("ausverkauft", out_title, recent["id"]), ("ausverkauft", out_title, recent["id"]),
) )
# Auto-mark matching neuheiten as sold out
if out_title:
mark_ausverkauft_in_neuheiten(db, out_title)
# Merge media paths (keep all, separated by ||) # Merge media paths (keep all, separated by ||)
if media_path: if media_path:
merged_media = media_path merged_media = media_path
@@ -772,6 +793,9 @@ def whatsapp_webhook():
VALUES (?, ?, ?, ?, ?, ?, 'new', ?, ?, ?)""", VALUES (?, ?, ?, ?, ?, ?, 'new', ?, ?, ?)""",
("whatsapp", sender_id, sender_name, msg_type, content, media_path, halle, cat, spiel_titel), ("whatsapp", sender_id, sender_name, msg_type, content, media_path, halle, cat, spiel_titel),
) )
# Auto-mark matching neuheiten as sold out
if cat == "ausverkauft" and spiel_titel:
mark_ausverkauft_in_neuheiten(db, spiel_titel)
# Auto-reply: only on NEW threads, not appends # Auto-reply: only on NEW threads, not appends
if not thread_found and META_TOKEN: if not thread_found and META_TOKEN:
@@ -789,7 +813,6 @@ def whatsapp_webhook():
) )
send_whatsapp_message(sender_id, reply) send_whatsapp_message(sender_id, reply)
# Thorsten voice: same text but without emojis # Thorsten voice: same text but without emojis
import re
tts_text = re.sub(r'[^\x00-\x7F\u00C0-\u00FF\u0100-\u017F\u0180-\u024F\u1E00-\u1EFF\u2018-\u201F\u2013\u2014\u2026\u00A0\u00AB\u00BB\u20AC]', '', reply) tts_text = re.sub(r'[^\x00-\x7F\u00C0-\u00FF\u0100-\u017F\u0180-\u024F\u1E00-\u1EFF\u2018-\u201F\u2013\u2014\u2026\u00A0\u00AB\u00BB\u20AC]', '', reply)
tts_text = re.sub(r' +', ' ', tts_text).strip() tts_text = re.sub(r' +', ' ', tts_text).strip()
audio_path = generate_thorsten_tts(tts_text) audio_path = generate_thorsten_tts(tts_text)
@@ -3213,7 +3236,7 @@ def api_chat():
params_n.extend(params_n[:]) # duplicate for WHERE clause params_n.extend(params_n[:]) # duplicate for WHERE clause
score_expr_n = ' + '.join(score_parts_n) score_expr_n = ' + '.join(score_parts_n)
sql_n = ( sql_n = (
f"SELECT titel, untertitel, halle, stand, info_text, themen, web, ({score_expr_n}) AS relevance " f"SELECT titel, untertitel, halle, stand, info_text, themen, web, flags, ({score_expr_n}) AS relevance "
"FROM neuheiten " "FROM neuheiten "
f"WHERE {' OR '.join(like_clauses_n)} " f"WHERE {' OR '.join(like_clauses_n)} "
"ORDER BY relevance DESC LIMIT 30" "ORDER BY relevance DESC LIMIT 30"
@@ -3235,12 +3258,43 @@ def api_chat():
halle_stand = f"{n['halle'] or ''} {n['stand'] or ''}".strip() halle_stand = f"{n['halle'] or ''} {n['stand'] or ''}".strip()
verlag = n['untertitel'] or '' verlag = n['untertitel'] or ''
themen = (n['themen'] or '').strip() themen = (n['themen'] or '').strip()
ausverkauft = 'AUSVERKAUFT' if (n['flags'] or 0) & 1 else ''
context_parts.append( context_parts.append(
f"[Neuheit] {n['titel']}" f"[Neuheit] {n['titel']}"
+ (f" | Verlag: {verlag}" if verlag else '') + (f" | Verlag: {verlag}" if verlag else '')
+ (f" | {halle_stand}" if halle_stand else '') + (f" | {halle_stand}" if halle_stand else '')
+ (f" | 🔴 {ausverkauft}" if ausverkauft else '')
+ (f" | {themen}" if themen else '') + (f" | {themen}" if themen else '')
) )
# Also search published submissions for ausverkauft stories with matching spiel_titel
if meaningful_terms:
ausverkauft_subs = []
try:
for t in meaningful_terms:
rows = db.execute(
"SELECT spiel_titel, content, halle, published_at FROM submissions "
"WHERE status='published' AND deleted_at IS NULL "
"AND category='ausverkauft' AND spiel_titel IS NOT NULL "
"AND LOWER(spiel_titel) LIKE ? LIMIT 5",
(f"%{t}%",),
).fetchall()
ausverkauft_subs.extend(rows)
except Exception:
pass
# Deduplicate by spiel_titel
seen_titles = set()
for r in ausverkauft_subs[:10]:
title = (r['spiel_titel'] or '').strip()
if title and title.lower() not in seen_titles:
seen_titles.add(title.lower())
halle = r['halle'] or ''
snippet = (r['content'] or '').strip()[:120]
context_parts.append(
f"[Ausverkauft-Meldung] {title}"
+ (f" | {halle}" if halle else '')
+ (f" | {snippet}" if snippet else '')
)
context = '\n'.join(context_parts) context = '\n'.join(context_parts)
# System prompt # System prompt
@@ -3256,6 +3310,9 @@ def api_chat():
"- Bei Standortfragen nenne immer die Hallen-Nummer.\n" "- Bei Standortfragen nenne immer die Hallen-Nummer.\n"
"- [Aussteller]-Einträge enthalten Verlage/Shops mit Halle, Stand und Land.\n" "- [Aussteller]-Einträge enthalten Verlage/Shops mit Halle, Stand und Land.\n"
"- [Neuheit]-Einträge enthalten Brettspiel-Neuheiten mit Verlag, Halle, Stand und Kategorien.\n" "- [Neuheit]-Einträge enthalten Brettspiel-Neuheiten mit Verlag, Halle, Stand und Kategorien.\n"
"- [Neuheit] mit 'AUSVERKAUFT' = am Stand vergriffen — erwähne das aktiv.\n"
"- [Ausverkauft-Meldung]-Einträge = Community-Berichte über vergriffene Spiele.\n"
"- Bei Fragen wie 'ist X noch da?' prüfe Ausverkauft-Status und warne.\n"
"- Wenn du einen Beitrag erwähnst, verlinke ihn IMMER mit der Beitrags-ID aus dem Kontext.\n" "- Wenn du einen Beitrag erwähnst, verlinke ihn IMMER mit der Beitrags-ID aus dem Kontext.\n"
" Format: [Kurzbeschreibung](/beitrag/ID)\n" " Format: [Kurzbeschreibung](/beitrag/ID)\n"
" Beispiel: 'Schau dir [diesen Erfahrungsbericht](/beitrag/53) an!'\n" " Beispiel: 'Schau dir [diesen Erfahrungsbericht](/beitrag/53) an!'\n"