longnick's picture
Make HF Telegram webhook reply through Telegram response method
ca0f6f3 verified
Raw
History Blame Contribute Delete
8.87 kB
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]})