🚀 Initial commit: BSN Chatbot — Multi-Channel Intake System

WhatsApp webhook, admin dashboard, public frontend, media handling, LLM auto-summarize, hall/stand detection, iOS-style backend
This commit is contained in:
Hermes Agent
2026-06-17 13:22:19 +02:00
commit bbefb8807b
6 changed files with 2191 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
.env
*.pyc
__pycache__/
media/
*.db
app.py.bak*
*.bak
.cloudflare_token
+1939
View File
File diff suppressed because it is too large Load Diff
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Initialize the BSN Intake SQLite database with schema + sample data."""
import sqlite3
import os
DB_PATH = os.path.join(os.path.dirname(__file__), "bsn_intake.db")
SCHEMA = """
-- Incoming submissions from all channels
CREATE TABLE IF NOT EXISTS submissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel TEXT NOT NULL, -- 'whatsapp', 'telegram', 'web_form'
sender_id TEXT NOT NULL, -- phone number, telegram ID, email
type TEXT NOT NULL, -- 'text', 'image', 'voice', 'video'
content TEXT, -- transcribed text or message body
media_path TEXT, -- local path to saved media file
status TEXT DEFAULT 'new', -- 'new', 'processing', 'done', 'flagged'
category TEXT, -- Hermes-classified category
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Knowledge base for the query chatbot
CREATE TABLE IF NOT EXISTS knowledge (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT UNIQUE NOT NULL, -- lookup key (e.g. 'autor_uwe_rosenberg')
question_pattern TEXT, -- "Where is Uwe Rosenberg?"
answer TEXT NOT NULL, -- "Hall 3, Booth B12, 10am-6pm"
category TEXT,
tags TEXT, -- comma-separated
valid_from DATE,
valid_until DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Indexes for fast lookups
CREATE INDEX IF NOT EXISTS idx_submissions_status ON submissions(status);
CREATE INDEX IF NOT EXISTS idx_submissions_channel ON submissions(channel);
CREATE INDEX IF NOT EXISTS idx_submissions_category ON submissions(category);
CREATE INDEX IF NOT EXISTS idx_knowledge_key ON knowledge(key);
CREATE INDEX IF NOT EXISTS idx_knowledge_category ON knowledge(category);
"""
SAMPLE_SUBMISSIONS = [
("whatsapp", "+491234567890", "text", "Wo ist der Stand von Feuerland? Hallo?", "new", None),
("whatsapp", "+491234567891", "text", "Catan ist ausverkauft in Halle 5!", "new", "ausverkauft"),
("telegram", "8520780873", "text", "Habe eben gesehen: Kosmos Halle 3, Stand A42 hat noch Restbestände von Die Crew", "new", "restbestände"),
("web_form", "besucher@example.com", "text", "Warum gibt es keine barrierefreien Zugänge in Halle 8?", "flagged", "kritik"),
]
SAMPLE_KNOWLEDGE = [
("feuerland_stand", "Wo ist Feuerland?", "Feuerland Spiele findest du in Halle 3, Stand C17. Geöffnet von 1018 Uhr.", "verlage", "feuerland, stand, messe", "2026-10-01", "2026-10-04"),
("catan_verfuegbar", "Ist Catan noch verfügbar?", "Catan war Stand 12.06.2026 in Halle 5 ausverkauft. Kosmos meldet Nachschub für Morgen früh.", "verlage", "catan, kosmos, ausverkauft", "2026-10-01", "2026-10-04"),
("oeffnungszeiten", "Wann öffnet die Messe?", "Die Spiel Essen 2026 öffnet Donnerstag bis Samstag von 1018 Uhr, Sonntag von 1017 Uhr.", "messe", "öffnungszeiten, messe, essen", None, None),
]
def init():
conn = sqlite3.connect(DB_PATH)
conn.executescript(SCHEMA)
# Check if data already exists
count = conn.execute("SELECT COUNT(*) FROM submissions").fetchone()[0]
if count == 0:
conn.executemany(
"INSERT INTO submissions (channel, sender_id, type, content, status, category) VALUES (?,?,?,?,?,?)",
SAMPLE_SUBMISSIONS,
)
print(f"Inserted {len(SAMPLE_SUBMISSIONS)} sample submissions")
count = conn.execute("SELECT COUNT(*) FROM knowledge").fetchone()[0]
if count == 0:
conn.executemany(
"INSERT INTO knowledge (key, question_pattern, answer, category, tags, valid_from, valid_until) VALUES (?,?,?,?,?,?,?)",
SAMPLE_KNOWLEDGE,
)
print(f"Inserted {len(SAMPLE_KNOWLEDGE)} sample knowledge entries")
conn.commit()
conn.close()
print(f"Database initialized: {DB_PATH}")
if __name__ == "__main__":
init()
Executable
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# Start the BSN Chatbot Intake server (uses venv Python for Flask + requests)
set -a
source "$(dirname "$0")/.env"
set +a
echo "META_TOKEN set: $([ -n "$META_TOKEN" ] && echo 'YES' || echo 'NO')"
echo "PHONE_NUMBER_ID: $META_PHONE_NUMBER_ID"
echo ""
exec /home/hermes/venv/bin/python3 "$(dirname "$0")/app.py"
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
# Start permanent Cloudflare Tunnel for BSN Chatbot
# Usage: bash start-tunnel.sh <CLOUDFLARE_TUNNEL_TOKEN>
set -e
TOKEN="$1"
if [ -z "$TOKEN" ]; then
echo "Usage: bash start-tunnel.sh <CLOUDFLARE_TUNNEL_TOKEN>"
exit 1
fi
# Save token
echo "$TOKEN" > /home/hermes/workspace/bsn-chatbot/.cloudflare_token
chmod 600 /home/hermes/workspace/bsn-chatbot/.cloudflare_token
echo "Token saved ($(wc -c < /home/hermes/workspace/bsn-chatbot/.cloudflare_token) chars)"
# Kill any existing cloudflared
pkill -9 -f cloudflared 2>/dev/null || true
sleep 1
# Start permanent tunnel
echo "Starting tunnel..."
exec /home/hermes/.local/bin/cloudflared tunnel run --token "$TOKEN"
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""
BSN Intake Supervisor — starts Flask app + Cloudflare tunnel.
Keeps both alive, restarts on crash, prints the public URL.
"""
import subprocess
import time
import os
import sys
import re
BASE_DIR = "/home/hermes/workspace/bsn-chatbot"
VENV_PYTHON = "/home/hermes/venv/bin/python3"
CLOUDFLARED = "/home/hermes/.local/bin/cloudflared"
# Load .env
env = os.environ.copy()
env_file = os.path.join(BASE_DIR, ".env")
if os.path.exists(env_file):
with open(env_file) as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key, _, val = line.partition("=")
key = key.strip()
val = val.strip().strip('"').strip("'")
env[key] = val
def start_flask():
"""Start the Flask app as a subprocess."""
proc = subprocess.Popen(
[VENV_PYTHON, os.path.join(BASE_DIR, "app.py")],
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
return proc
def start_cloudflared():
"""Start the Cloudflare tunnel."""
proc = subprocess.Popen(
[CLOUDFLARED, "tunnel", "--url", "http://127.0.0.1:5002"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
return proc
def main():
LOG = open("/tmp/bsn_supervisor.log", "w")
def log(msg):
LOG.write(msg + "\n")
LOG.flush()
log("🚀 BSN Intake Supervisor starting...")
# Start Flask
flask = start_flask()
log("[flask] started")
time.sleep(2)
# Verify Flask is up
import urllib.request
try:
urllib.request.urlopen("http://127.0.0.1:5002/health", timeout=5)
log("[flask] health check: OK")
except Exception as e:
log(f"[flask] health check FAILED: {e}")
# Start Cloudflared
cf = start_cloudflared()
log("[cloudflared] started")
# Wait for tunnel URL to appear in output
tunnel_url = None
deadline = time.time() + 20
buffer = ""
while time.time() < deadline:
line = cf.stdout.readline() if cf.stdout else ""
if line:
buffer += line
m = re.search(r"(https://[a-z-]+\.trycloudflare\.com)", buffer)
if m:
tunnel_url = m.group(1)
break
if flask.poll() is not None:
log(f"[flask] CRASHED! exit={flask.returncode}")
break
if cf.poll() is not None:
log(f"[cloudflared] CRASHED! exit={cf.returncode}")
break
if tunnel_url:
log(f"\n✅ TUNNEL: {tunnel_url}")
log(f" Webhook: {tunnel_url}/whatsapp/webhook")
log(f" Admin: {tunnel_url}/admin")
else:
log("❌ Failed to get tunnel URL")
log("\n[supervisor] Running. Press Ctrl+C to stop.\n")
# Keep running, restart children if they die
try:
while True:
if flask.poll() is not None:
log("[flask] died, restarting...")
flask = start_flask()
if cf.poll() is not None:
log("[cloudflared] died, restarting...")
cf = start_cloudflared()
time.sleep(5)
except KeyboardInterrupt:
log("\n[supervisor] Shutting down...")
flask.terminate()
cf.terminate()
flask.wait()
cf.wait()
if __name__ == "__main__":
main()