| """ |
| app.py - Hauptanwendung (FastAPI) für den Mama Bot |
| Läuft auf Hugging Face Spaces (Docker). |
| Nutzt Hugging Face Inference API für das LLM. |
| WhatsApp-Anbindung über WAHA (WhatsApp HTTP API). |
| |
| Enthält: |
| - WhatsApp Webhook-Handler mit Intent-Erkennung |
| - Keep-Alive System (Self-Ping + externe Endpoints) |
| - Integrierter Scheduler (Morgennachrichten, Check-in, Wochenprogramm) |
| - Reminder-System |
| - Benutzerprofil-Verwaltung |
| """ |
|
|
| import os |
| import json |
| import random |
| import logging |
| import threading |
| import time as _time |
| from datetime import datetime |
|
|
| import httpx |
| from fastapi import FastAPI, Request, HTTPException |
| from fastapi.responses import JSONResponse, PlainTextResponse |
|
|
| from bot_config import ( |
| EMERGENCY_KEYWORDS, AUTHORITY_KEYWORDS, HEALTH_KEYWORDS, REMINDER_KEYWORDS, |
| EMERGENCY_NUMBERS, |
| FALLBACK_MESSAGES, get_system_prompt, load_user_profile, save_user_profile, |
| MAX_HISTORY, TOPICS, MORNING_TEMPLATES, CHECKIN_MESSAGES, WEEKLY_PROGRAM_MESSAGES |
| ) |
|
|
| |
| |
| |
| logging.basicConfig( |
| level=logging.INFO, |
| format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" |
| ) |
| logger = logging.getLogger("mama-bot") |
|
|
| |
| |
| |
| app = FastAPI( |
| title="Mama Bot", |
| version="3.0.0", |
| description="Persönlicher KI-Assistent für ältere Personen – Hugging Face Spaces Edition" |
| ) |
|
|
| |
| |
| |
| HF_API_TOKEN = os.getenv("HF_API_TOKEN", os.getenv("HF_TOKEN", "")) |
| HF_MODEL = os.getenv("HF_MODEL", "mistralai/Mistral-7B-Instruct-v0.3") |
|
|
| |
| GREEN_API_URL = os.getenv("GREEN_API_URL", "https://7107.api.greenapi.com") |
| GREEN_API_ID_INSTANCE = os.getenv("GREEN_API_ID_INSTANCE", "7107662501") |
| GREEN_API_TOKEN = os.getenv("GREEN_API_TOKEN", "") |
|
|
| |
| WAHA_API_URL = os.getenv("WAHA_API_URL", "http://waha:3000") |
| WAHA_API_KEY = os.getenv("WAHA_API_KEY", "") |
| WAHA_SESSION = os.getenv("WAHA_SESSION", "default") |
|
|
| USER_NAME = os.getenv("USER_NAME", "Sabine") |
| USER_PHONE = os.getenv("USER_PHONE", "") |
|
|
| |
| MORNING_HOUR = int(os.getenv("MORNING_HOUR", 8)) |
| MORNING_MINUTE = int(os.getenv("MORNING_MINUTE", 0)) |
| CHECKIN_DAYS = int(os.getenv("CHECKIN_DAYS", 3)) |
|
|
| |
| SCHEDULER_STATE_FILE = os.path.join(os.path.dirname(__file__), "memory", "scheduler_state.json") |
|
|
| |
| |
| |
| conversation_history: list[dict] = [] |
| reminders: list[dict] = [] |
|
|
| |
| |
| |
|
|
| def load_scheduler_state() -> dict: |
| """Lädt den Scheduler-Zustand aus der JSON-Datei.""" |
| try: |
| with open(SCHEDULER_STATE_FILE, "r", encoding="utf-8") as f: |
| return json.load(f) |
| except (FileNotFoundError, json.JSONDecodeError): |
| return { |
| "last_morning": None, |
| "last_checkin": None, |
| "sent_topic_ids": [] |
| } |
|
|
|
|
| def save_scheduler_state(state: dict): |
| """Speichert den Scheduler-Zustand.""" |
| os.makedirs(os.path.dirname(SCHEDULER_STATE_FILE), exist_ok=True) |
| with open(SCHEDULER_STATE_FILE, "w", encoding="utf-8") as f: |
| json.dump(state, f, ensure_ascii=False, indent=2) |
|
|
|
|
| |
| |
| |
|
|
| def call_hf_inference(system_prompt: str, user_message: str, history: list[dict]) -> str: |
| """Ruft die Hugging Face Inference API auf.""" |
| if not HF_API_TOKEN: |
| logger.warning("Kein HF_API_TOKEN gesetzt – nutze Fallback.") |
| return random.choice(FALLBACK_MESSAGES) |
|
|
| url = f"https://api-inference.huggingface.co/models/{HF_MODEL}" |
| prompt_parts = [f"[INST] <<SYS>>\n{system_prompt}\n<</SYS>>\n\n"] |
|
|
| for h in history[-MAX_HISTORY:]: |
| if h["role"] == "user": |
| prompt_parts.append(f"{h['content']} [/INST] ") |
| elif h["role"] == "assistant": |
| prompt_parts.append(f"{h['content']} [INST] ") |
|
|
| prompt_parts.append(f"{user_message} [/INST]") |
| full_prompt = "".join(prompt_parts) |
|
|
| headers = {"Authorization": f"Bearer {HF_API_TOKEN}", "Content-Type": "application/json"} |
| payload = { |
| "inputs": full_prompt, |
| "parameters": { |
| "max_new_tokens": 350, |
| "temperature": 0.7, |
| "top_p": 0.9, |
| "repetition_penalty": 1.1, |
| "return_full_text": False |
| } |
| } |
|
|
| try: |
| with httpx.Client(timeout=60) as client: |
| response = client.post(url, json=payload, headers=headers) |
| if response.status_code == 503: |
| logger.info("Modell wird geladen, warte 20s...") |
| _time.sleep(20) |
| response = client.post(url, json=payload, headers=headers) |
| response.raise_for_status() |
| data = response.json() |
| if isinstance(data, list) and len(data) > 0: |
| generated = data[0].get("generated_text", "") |
| return generated.strip() if generated.strip() else random.choice(FALLBACK_MESSAGES) |
| return random.choice(FALLBACK_MESSAGES) |
| except httpx.TimeoutException: |
| logger.error("Timeout bei HF Inference API") |
| return "Entschuldige, das hat etwas gedauert. Kannst du das bitte wiederholen? 😊" |
| except Exception as e: |
| logger.error(f"Fehler bei HF Inference API: {e}") |
| return random.choice(FALLBACK_MESSAGES) |
|
|
|
|
| |
| |
| |
|
|
| def send_whatsapp(phone: str, message: str) -> bool: |
| """Sendet eine WhatsApp-Nachricht über Green API.""" |
| if not phone: |
| return False |
| try: |
| url = f"{GREEN_API_URL}/waInstance{GREEN_API_ID_INSTANCE}/sendMessage/{GREEN_API_TOKEN}" |
| payload = { |
| "chatId": f"{phone}@s.whatsapp.net", |
| "message": message |
| } |
| with httpx.Client(timeout=30) as client: |
| resp = client.post(url, json=payload) |
| data = resp.json() |
| return data.get("idMessage", "") != "" |
| except Exception as e: |
| logger.error(f"Fehler beim WhatsApp-Senden (Green API): {e}") |
| return False |
|
|
|
|
| def receive_whatsapp_webhook(data: dict) -> tuple: |
| """Parst eine Green API Webhook-Nachricht.""" |
| try: |
| |
| message_body = data.get("message", {}).get("text", "") |
| sender = data.get("sender", {}).get("id", "").replace("@s.whatsapp.net", "") |
| return message_body, sender |
| except Exception as e: |
| logger.error(f"Fehler beim Parsen der Green API Nachricht: {e}") |
| return "", "" |
|
|
|
|
| |
| |
| |
|
|
| def detect_intent(message: str) -> str: |
| """Erkennt die Absicht: emergency, authority, health, reminder, chat.""" |
| msg = message.lower().strip() |
| for kw in EMERGENCY_KEYWORDS: |
| if kw in msg: |
| return "emergency" |
| for kw in HEALTH_KEYWORDS: |
| if kw in msg: |
| return "health" |
| for kw in AUTHORITY_KEYWORDS: |
| if kw in msg: |
| return "authority" |
| for kw in REMINDER_KEYWORDS: |
| if kw in msg: |
| return "reminder" |
| return "chat" |
|
|
|
|
| |
| |
| |
|
|
| def handle_emergency() -> str: |
| numbers = "\n".join(f"📞 {n}: {nr}" for n, nr in EMERGENCY_NUMBERS.items()) |
| return ( |
| "🚨 Ich bin hier, um dir zu helfen!\n\n" |
| "Atme erst einmal tief durch. Alles wird gut.\n\n" |
| f"Wichtige Telefonnummern:\n{numbers}\n\n" |
| "Wenn es ein medizinischer Notfall ist, wähle bitte direkt die 112.\n" |
| "Sag mir genau, was passiert ist, dann helfe ich dir weiter. 💝" |
| ) |
|
|
|
|
| def handle_authority(user_message: str) -> str: |
| msg = user_message.lower() |
| disclaimer = "⚠️ Das sind allgemeine Informationen, kein Rechtsrat. Bei wichtigen Entscheidungen bitte Beratung suchen." |
| if "widerspruch" in msg: |
| return ( |
| "📋 Einen Widerspruch kannst du innerhalb von 1 Monat nach Zustellung des Bescheids einlegen.\n\n" |
| "Schreibe einen Brief, in dem du erklärst, warum du nicht einverstanden bist, " |
| "und schicke ihn an die Behörde, die den Bescheid geschickt hat.\n\n" |
| "💡 Kostenlose Beratung: VdK 0800 888 0555\n\n" |
| f"{disclaimer}" |
| ) |
| if "rente" in msg: |
| return ( |
| "💰 Den Rentenantrag solltest du 3 Monate vor deinem Rentenbeginn stellen.\n\n" |
| "Deutsche Rentenversicherung: 0800 1000 100\n" |
| "VdK-Beratung: 0800 888 0555\n\n" |
| f"{disclaimer}" |
| ) |
| if "pflegegrad" in msg: |
| return ( |
| "🏥 Den Pflegegrad beantragst du bei deiner Krankenkasse.\n" |
| "Du brauchst einen Arztbericht. Der MDK kommt zu dir und schaut, wie viel Hilfe du brauchst.\n\n" |
| "💡 Beratung: VdK 0800 888 0555\n\n" |
| f"{disclaimer}" |
| ) |
| return ( |
| "🏛️ Kostenlose Beratung:\n" |
| "• VdK: 0800 888 0555\n" |
| "• SoVD: 0800 554 5555\n" |
| "• Deutsche Rentenversicherung: 0800 1000 100\n\n" |
| "Wichtige Fristen:\n" |
| "• Widerspruch: 1 Monat nach Bescheid\n" |
| "• Rente beantragen: 3 Monate vor Rentenbeginn\n\n" |
| f"{disclaimer}" |
| ) |
|
|
|
|
| def handle_health(user_message: str) -> str: |
| msg = user_message.lower() |
| if "pflegegeld" in msg or "sachleistung" in msg: |
| return ( |
| "💊 Es gibt zwei Arten der Pflegeunterstützung:\n\n" |
| "1. Pflegegeld: Geld für die häusliche Pflege durch Angehörige.\n" |
| "2. Pflegesachleistung: Ein Pflegedienst kommt zu dir nach Hause.\n\n" |
| "Du kannst auch beides mischen (Kombinationsleistung).\n\n" |
| "💡 Nach § 45b SGB XI hast du auch Anspruch auf Entlastungsleistung " |
| "(bis zu 120,80 € pro Monat).\n\n" |
| "Ruf deine Krankenkasse an oder geh zum Pflegestützpunkt vor Ort. 💝" |
| ) |
| if "mdk" in msg or "gutachtung" in msg: |
| return ( |
| "🏥 Der MDK (Medizinischer Dienst) schaut bei deinem Pflegegrad-Antrag.\n\n" |
| "So läuft's ab:\n" |
| "1. Antrag bei der Krankenkasse\n" |
| "2. Arztbericht erstellen\n" |
| "3. MDK-Besuch bei dir zu Hause\n" |
| "4. Pflegegrad-Bescheid (1-5)\n\n" |
| "Das dauert meist 4-6 Wochen. Hab etwas Geduld! 😊" |
| ) |
| return ( |
| "💚 Gesundheit ist das Wichtigste!\n\n" |
| "Anlaufstellen:\n" |
| "• Deine Krankenkasse (Nummer auf der Versichertenkarte)\n" |
| "• Pflegestützpunkt vor Ort\n" |
| "• VdK-Beratung: 0800 888 0555\n" |
| "• Ärztlicher Notdienst: 116 117\n\n" |
| "Wenn du Schmerzen hast, ruf bitte den Arzt an. " |
| "Ich bin nur ein Computerprogramm und kann keine medizinische Beratung geben. 💝" |
| ) |
|
|
|
|
| def handle_reminder(user_message: str) -> str: |
| reminder_id = str(datetime.now().timestamp()).replace(".", "") |
| reminders.append({ |
| "id": reminder_id, |
| "message": user_message, |
| "created_at": datetime.now().isoformat(), |
| "status": "pending" |
| }) |
| return f"✅ Alles klar! Ich habe mir notiert: \"{user_message}\"\n\nIch werde dich daran erinnern! 📝" |
|
|
|
|
| def handle_chat(user_message: str, history: list) -> str: |
| """Normaler Chat – ruft HF Inference API auf.""" |
| profile = load_user_profile() |
| system_prompt = get_system_prompt( |
| user_name=profile.get("name", USER_NAME), |
| preferences=profile.get("preferences", []), |
| concerns=profile.get("concerns", []) |
| ) |
| return call_hf_inference(system_prompt, user_message, history) |
|
|
|
|
| |
| |
| |
|
|
| @app.post("/webhook") |
| async def webhook(request: Request): |
| """Empfängt WhatsApp-Nachrichten von WAHA und sendet Antworten.""" |
| try: |
| data = await request.json() |
| logger.info(f"Webhook empfangen: {json.dumps(data, ensure_ascii=False)[:500]}") |
|
|
| |
| message_body, sender = receive_whatsapp_webhook(data) |
| |
| if not message_body: |
| message_body = data.get("body", data.get("text", "")) |
| if not sender: |
| sender = data.get("from", data.get("sender", "")) |
|
|
| if not message_body: |
| return JSONResponse({"status": "ok", "message": "Keine Nachricht"}) |
|
|
| intent = detect_intent(message_body) |
| logger.info(f"Intent: {intent}") |
|
|
| if intent == "emergency": |
| response_text = handle_emergency() |
| elif intent == "authority": |
| response_text = handle_authority(message_body) |
| elif intent == "health": |
| response_text = handle_health(message_body) |
| elif intent == "reminder": |
| response_text = handle_reminder(message_body) |
| else: |
| conversation_history.append({"role": "user", "content": message_body}) |
| response_text = handle_chat(message_body, conversation_history) |
|
|
| conversation_history.append({"role": "assistant", "content": response_text}) |
|
|
| if len(conversation_history) > MAX_HISTORY * 2: |
| conversation_history[:] = conversation_history[-MAX_HISTORY:] |
|
|
| profile = load_user_profile() |
| profile["last_contact"] = datetime.now().isoformat() |
| save_user_profile(profile) |
|
|
| if sender: |
| send_whatsapp(sender, response_text) |
|
|
| return JSONResponse({"status": "ok", "reply": response_text, "intent": intent}) |
|
|
| except Exception as e: |
| logger.error(f"Webhook-Fehler: {e}") |
| return JSONResponse({"status": "error", "message": str(e)}) |
|
|
|
|
| |
| |
| |
|
|
| @app.get("/") |
| async def root(): |
| return { |
| "status": "running", |
| "service": "Mama Bot", |
| "version": "3.0.0", |
| "model": HF_MODEL, |
| "timestamp": datetime.now().isoformat() |
| } |
|
|
|
|
| @app.get("/health") |
| async def health(): |
| state = load_scheduler_state() |
| return { |
| "status": "healthy", |
| "model": HF_MODEL, |
| "hf_token_set": bool(HF_API_TOKEN), |
| "whatsapp": "green-api", |
| "green_api_instance": GREEN_API_ID_INSTANCE, |
| "conversation_length": len(conversation_history), |
| "active_reminders": len([r for r in reminders if r["status"] == "pending"]), |
| "last_morning": state.get("last_morning"), |
| "last_checkin": state.get("last_checkin"), |
| "sent_topics_count": len(state.get("sent_topic_ids", [])), |
| "timestamp": datetime.now().isoformat() |
| } |
|
|
|
|
| |
| |
| |
|
|
| _last_self_ping = datetime.now() |
|
|
|
|
| @app.get("/keepalive") |
| async def keepalive(): |
| """ |
| Keep-Alive Endpoint für externe Ping-Services. |
| Rufen Sie diesen Endpoint alle 30-60 Minuten auf, |
| um den Space aktiv zu halten. |
| |
| Empfohlene externe Services (kostenlos): |
| - cron-job.org (kostenlos, bis 10 Jobs) |
| - UptimeRobot (kostenlos, 5-Minuten-Intervall) |
| - Freshping (kostenlos, 1-Minuten-Intervall) |
| """ |
| global _last_self_ping |
| _last_self_ping = datetime.now() |
| return { |
| "status": "alive", |
| "service": "Mama Bot", |
| "message": "Space ist wach! 💚", |
| "timestamp": datetime.now().isoformat() |
| } |
|
|
|
|
| @app.get("/ping") |
| async def ping(): |
| """Einfacher Ping-Endpoint (kompatibel mit UptimeRobot etc.).""" |
| return "pong" |
|
|
|
|
| |
| |
| |
|
|
| def load_topics(topic_key: str) -> list: |
| """Lädt Themen aus der JSON-Datei.""" |
| cfg = TOPICS.get(topic_key) |
| if not cfg: |
| return [] |
| path = os.path.join(os.path.dirname(__file__), cfg["file"]) |
| try: |
| with open(path, "r", encoding="utf-8") as f: |
| return json.load(f).get("topics", []) |
| except (FileNotFoundError, json.JSONDecodeError) as e: |
| logger.error(f"Themen laden fehlgeschlagen ({topic_key}): {e}") |
| return [] |
|
|
|
|
| def get_today_topic_key() -> str: |
| """Bestimmt das Thema basierend auf dem Wochentag.""" |
| keys = list(TOPICS.keys()) |
| return keys[datetime.now().weekday() % len(keys)] |
|
|
|
|
| def generate_morning_message() -> str: |
| """Generiert die tägliche Morgennachricht.""" |
| profile = load_user_profile() |
| user_name = profile.get("name", USER_NAME) |
| state = load_scheduler_state() |
|
|
| topic_key = get_today_topic_key() |
| topic_cfg = TOPICS[topic_key] |
| emoji = topic_cfg["emoji"] |
| topic_name = topic_cfg["name"] |
|
|
| all_topics = load_topics(topic_key) |
| available = [t for t in all_topics if t.get("id") not in state.get("sent_topic_ids", [])] |
|
|
| if not available: |
| state["sent_topic_ids"] = [] |
| save_scheduler_state(state) |
| available = all_topics |
|
|
| if available: |
| chosen = random.choice(available) |
| topic_id = chosen.get("id", "") |
| state.setdefault("sent_topic_ids", []).append(topic_id) |
| save_scheduler_state(state) |
| base_content = chosen.get("content", "") |
| else: |
| base_content = f"Heute geht es um {topic_name}! {emoji}" |
|
|
| |
| system = ( |
| "Du bist ein liebevoller, geduldiger Assistent für eine ältere Person. " |
| "Schreibe eine kurze, warme Morgennachricht auf Deutsch (max. 3 Sätze). " |
| "Du verwendest einfache Wörter, ein freundliches 'Du' und passende Emojis. " |
| "Die Person heißt Sabine – sprich sie direkt mit Namen an." |
| ) |
| prompt = ( |
| f"Schreibe eine Morgennachricht für {user_name}. " |
| f"Thema heute: {emoji} {topic_name}. " |
| f"Info: {base_content}" |
| ) |
|
|
| generated = call_hf_inference(system, prompt, []) |
|
|
| if generated and len(generated) > 20: |
| return generated |
|
|
| |
| template = random.choice(MORNING_TEMPLATES) |
| return template.format(name=user_name, content=base_content) |
|
|
|
|
| def generate_weekly_message() -> str: |
| """Generiert die Montags-Frage für das Wochenprogramm.""" |
| profile = load_user_profile() |
| user_name = profile.get("name", USER_NAME) |
| return random.choice(WEEKLY_PROGRAM_MESSAGES).format(name=user_name) |
|
|
|
|
| def generate_checkin_message() -> str: |
| """Generiert eine sanfte Check-in Nachricht.""" |
| profile = load_user_profile() |
| user_name = profile.get("name", USER_NAME) |
| return random.choice(CHECKIN_MESSAGES).format(name=user_name) |
|
|
|
|
| def check_morning(): |
| """Prüft und sendet die Morgennachricht.""" |
| now = datetime.now() |
| state = load_scheduler_state() |
|
|
| |
| last = state.get("last_morning") |
| if last and datetime.fromisoformat(last).date() == now.date(): |
| return |
|
|
| |
| if now.hour != MORNING_HOUR or now.minute < MORNING_MINUTE: |
| return |
|
|
| if now.weekday() == 0: |
| msg = generate_weekly_message() |
| logger.info("Sende Wochenprogramm (Montag)...") |
| else: |
| msg = generate_morning_message() |
| logger.info("Sende Morgennachricht...") |
|
|
| if USER_PHONE: |
| ok = send_whatsapp(USER_PHONE, msg) |
| if ok: |
| state["last_morning"] = now.isoformat() |
| save_scheduler_state(state) |
| logger.info("Morgennachricht gesendet!") |
| else: |
| logger.error("Morgennachricht senden fehlgeschlagen.") |
| else: |
| logger.info(f"[KEIN PHONE] Nachricht: {msg}") |
|
|
|
|
| def check_checkin(): |
| """Prüft ob Check-in nötig ist und sendet.""" |
| profile = load_user_profile() |
| last_contact = profile.get("last_contact") |
| if not last_contact: |
| return |
|
|
| days = (datetime.now() - datetime.fromisoformat(last_contact)).days |
| if days >= CHECKIN_DAYS: |
| msg = generate_checkin_message() |
| if USER_PHONE: |
| ok = send_whatsapp(USER_PHONE, msg) |
| if ok: |
| logger.info(f"Check-in nach {days} Tagen gesendet.") |
| else: |
| logger.error("Check-in senden fehlgeschlagen.") |
| else: |
| logger.info(f"Letzter Kontakt {days} Tage her. Kein Check-in.") |
|
|
|
|
| |
| |
| |
|
|
| def _keep_alive_loop(): |
| """ |
| Keep-Alive Thread: Pingt sich selbst alle 30 Minuten |
| und führt die Scheduler-Aufgaben aus. |
| """ |
| |
| _time.sleep(120) |
|
|
| last_scheduler_check = datetime.now() |
|
|
| while True: |
| try: |
| |
| port = int(os.getenv("PORT", 7860)) |
| url = f"http://127.0.0.1:{port}/keepalive" |
| with httpx.Client(timeout=10) as client: |
| resp = client.get(url) |
| if resp.status_code == 200: |
| logger.info("✅ Keep-Alive Ping erfolgreich – Space bleibt wach.") |
| else: |
| logger.warning(f"Keep-Alive Ping fehlgeschlagen: {resp.status_code}") |
| except Exception as e: |
| logger.warning(f"Keep-Alive Ping Fehler: {e}") |
|
|
| |
| now = datetime.now() |
| if (now - last_scheduler_check).total_seconds() >= 300: |
| last_scheduler_check = now |
| try: |
| check_morning() |
| check_checkin() |
| except Exception as e: |
| logger.error(f"Scheduler-Fehler: {e}") |
|
|
| |
| _time.sleep(1800) |
|
|
|
|
| def start_background_threads(): |
| """Startet alle Hintergrund-Threads.""" |
| |
| t = threading.Thread(target=_keep_alive_loop, daemon=True, name="keep-alive-scheduler") |
| t.start() |
| logger.info("🔄 Keep-Alive + Scheduler Thread gestartet.") |
|
|
|
|
| |
| |
| |
|
|
| @app.get("/profile") |
| async def get_profile(): |
| return load_user_profile() |
|
|
|
|
| @app.put("/profile") |
| async def update_profile(request: Request): |
| data = await request.json() |
| save_user_profile(data) |
| return {"status": "ok", "profile": data} |
|
|
|
|
| @app.get("/reminders") |
| async def list_reminders(): |
| active = [r for r in reminders if r["status"] == "pending"] |
| return {"reminders": active, "count": len(active)} |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| import uvicorn |
| port = int(os.getenv("PORT", 7860)) |
|
|
| |
| start_background_threads() |
|
|
| uvicorn.run(app, host="0.0.0.0", port=port) |
|
|