| 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) |
| |
| |
| return JSONResponse({"method": "sendMessage", "chat_id": chat_id, "text": reply[:3900]}) |
|
|