import json import os import platform import re import time import urllib.error import urllib.parse import urllib.request from datetime import datetime, timezone from pathlib import Path from typing import Any from fastapi import FastAPI, Request from fastapi.responses import JSONResponse, PlainTextResponse STARTED_AT = time.time() APP_NAME = "hermes-fallback-worker" BASE_DIR = Path(__file__).resolve().parent SKILLS_DIR = BASE_DIR / "skills" MODEL_CHAIN = BASE_DIR / "model_chain.yaml" ALLOWED_DEFAULT = "6496387732" app = FastAPI(title="Hermes Fallback Worker") def skill_stats() -> dict[str, Any]: skills = sorted(str(p.relative_to(SKILLS_DIR.parent)) for p in SKILLS_DIR.rglob("SKILL.md")) if SKILLS_DIR.exists() else [] return {"skills_dir": str(SKILLS_DIR), "skill_count": len(skills), "sample_skills": skills[:15]} def model_chain_stats() -> dict[str, Any]: if not MODEL_CHAIN.exists(): return {"model_chain_present": False} text = MODEL_CHAIN.read_text(encoding="utf-8", errors="replace") key_envs = sorted(set(re.findall(r"(?m)^\s*key_env:\s*([A-Z0-9_]+)\s*$", text))) present = [name for name in key_envs if os.getenv(name)] return { "model_chain_present": True, "model_chain_bytes": len(text.encode("utf-8")), "fallback_provider_entries": text.count("\n- provider:"), "custom_provider_entries": text.count("\n- name:"), "model_key_envs_required": len(key_envs), "model_key_envs_present": len(present), "model_key_envs_missing": [name for name in key_envs if name not in present], "hf_fallback_bot_configured": bool(os.getenv("HERMES_FALLBACK_BOT_TOKEN") or os.getenv("TELEGRAM_BOT_TOKEN")), } def status_payload() -> dict[str, Any]: payload = { "ok": True, "app": APP_NAME, "role": "hf-free-telegram-webhook-worker", "utc": datetime.now(timezone.utc).isoformat(), "uptime_s": int(time.time() - STARTED_AT), "python": platform.python_version(), "platform": platform.platform(), "pid": os.getpid(), "state": "stateless", "note": "HF Free CPU Basic can still sleep; external keepalive is configured from the home server.", } payload.update(skill_stats()) payload.update(model_chain_stats()) return payload def allowed_chat(chat_id: int) -> bool: allowed = os.getenv("TELEGRAM_ALLOWED_USERS") or os.getenv("TELEGRAM_HOME_CHANNEL") or ALLOWED_DEFAULT ids = {part.strip() for part in re.split(r"[,\s]+", allowed) if part.strip()} return not ids or str(chat_id) in ids def telegram_api(method: str, data: dict[str, Any], timeout: int = 25) -> dict[str, Any]: token = os.getenv("HERMES_FALLBACK_BOT_TOKEN") or os.getenv("TELEGRAM_BOT_TOKEN") if not token: raise RuntimeError("missing Telegram bot token") body = urllib.parse.urlencode(data).encode() req = urllib.request.Request(f"https://api.telegram.org/bot{token}/{method}", data=body) with urllib.request.urlopen(req, timeout=timeout) as r: return json.loads(r.read().decode("utf-8")) def send_telegram(chat_id: int, text: str) -> dict[str, Any]: return telegram_api("sendMessage", {"chat_id": chat_id, "text": text[:3900]}, timeout=25) def post_json(url: str, payload: dict[str, Any], headers: dict[str, str], timeout: int = 45) -> dict[str, Any]: req = urllib.request.Request(url, data=json.dumps(payload).encode(), headers={"Content-Type": "application/json", **headers}) with urllib.request.urlopen(req, timeout=timeout) as r: return json.loads(r.read().decode("utf-8")) def chat_openai_compatible(base_url: str, key: str, model: str, messages: list[dict[str, str]], timeout: int = 45) -> str: data = post_json( base_url.rstrip("/") + "/chat/completions", {"model": model, "messages": messages, "temperature": 0.4, "max_tokens": 700}, {"Authorization": "Bearer " + key}, timeout=timeout, ) return (data.get("choices") or [{}])[0].get("message", {}).get("content", "").strip() def chat_gemini(key: str, model: str, messages: list[dict[str, str]], timeout: int = 45) -> str: text = "\n".join(f"{m['role']}: {m['content']}" for m in messages) url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={urllib.parse.quote(key)}" data = post_json(url, {"contents": [{"parts": [{"text": text}]}], "generationConfig": {"temperature": 0.4, "maxOutputTokens": 700}}, {}, timeout=timeout) return (((data.get("candidates") or [{}])[0].get("content") or {}).get("parts") or [{}])[0].get("text", "").strip() def ai_reply(user_text: str) -> str: system = ( "You are Hermes fallback running on Hugging Face Spaces for Vu Long. " "Reply concisely in Vietnamese unless the user asks otherwise. " "Be transparent that this is the HF fallback worker, not the full home server." ) messages = [{"role": "system", "content": system}, {"role": "user", "content": user_text[:4000]}] candidates = [ ("nvidia", "https://integrate.api.nvidia.com/v1", os.getenv("NVIDIA_API_KEY"), "nvidia/llama-3.3-nemotron-super-49b-v1"), ("nvidia2", "https://integrate.api.nvidia.com/v1", os.getenv("NVIDIA_API_KEY_2"), "nvidia/nemotron-3-nano-30b-a3b"), ("openrouter", "https://openrouter.ai/api/v1", os.getenv("OPENROUTER_API_KEY"), "openai/gpt-oss-120b:free"), ("groq", "https://api.groq.com/openai/v1", os.getenv("GROQ_API_KEY"), "llama-3.1-8b-instant"), ("cerebras", "https://api.cerebras.ai/v1", os.getenv("CEREBRAS_API_KEY"), "gpt-oss-120b"), ("mistral", "https://api.mistral.ai/v1", os.getenv("MISTRAL_API_KEY"), "mistral-small-latest"), ("deepseek", "https://api.deepseek.com/v1", os.getenv("DEEPSEEK_API_KEY"), "deepseek-chat"), ("github", "https://models.github.ai/inference", os.getenv("GITHUB_MODELS_API_KEY"), "openai/gpt-4.1-mini"), ] errors = [] for name, base, key, model in candidates: if not key: continue try: out = chat_openai_compatible(base, key, model, messages) if out: return out + f"\n\n— HF fallback via {name}/{model}" errors.append(name + ": empty") except Exception as exc: errors.append(name + ": " + type(exc).__name__) gemini_key = os.getenv("GEMINI_API_KEY") if gemini_key: try: out = chat_gemini(gemini_key, "gemini-2.5-flash", messages) if out: return out + "\n\n— HF fallback via gemini/gemini-2.5-flash" errors.append("gemini: empty") except Exception as exc: errors.append("gemini: " + type(exc).__name__) return "HF fallback worker nhận được tin nhắn nhưng chưa gọi được model nào. Lỗi đã được ghi nhận: " + ", ".join(errors[-5:]) @app.get("/") def root(): return PlainTextResponse("Hermes HF fallback worker is running. Use /healthz or Telegram webhook.") @app.get("/healthz") def healthz(): return status_payload() @app.post("/run_task") async def run_task(request: Request): body = await request.json() task = str(body.get("task") or "").strip() if not task: return JSONResponse({"ok": False, "error": "empty task"}, status_code=400) return {"ok": True, "reply": ai_reply(task), "status": status_payload()} @app.post("/telegram_webhook") async def telegram_webhook(request: Request): secret = os.getenv("HERMES_TELEGRAM_WEBHOOK_SECRET", "") if secret and request.headers.get("x-telegram-bot-api-secret-token") != secret: return PlainTextResponse("unauthorized", status_code=401) update = await request.json() message = update.get("message") or update.get("edited_message") or {} chat = message.get("chat") or {} chat_id = chat.get("id") text = (message.get("text") or "").strip() if not chat_id or not text: return {"ok": True, "ignored": True} chat_id = int(chat_id) if not allowed_chat(chat_id): return JSONResponse({"method": "sendMessage", "chat_id": chat_id, "text": "HF fallback worker: chat này chưa được phép."}) if text.startswith("/start") or text.startswith("/health"): s = status_payload() reply = f"HF fallback worker đang chạy. skills={s.get('skill_count')} fallbacks={s.get('fallback_provider_entries')} keys={s.get('model_key_envs_present')}/{s.get('model_key_envs_required')} uptime={s.get('uptime_s')}s" else: reply = ai_reply(text) # Do not call Telegram from inside HF; HF outbound to api.telegram.org is unreliable. # Telegram supports method calls in webhook responses and will send this message itself. return JSONResponse({"method": "sendMessage", "chat_id": chat_id, "text": reply[:3900]})