add files
This commit is contained in:
1 parent
ab8adbfa12
commit
fbb31409da
8 files changed
+775
No files matched your search
@@ -0,0 +1,204 @@
|
||||
"""Client tới Hermes Agent — kênh Zalo riêng (không phải support KB).
|
||||
|
||||
Backend mặc định: Hermes API Server (OpenAI-compatible) trên gateway :8642.
|
||||
Đọc env lúc gọi (không snapshot lúc import) để load_dotenv() luôn có hiệu lực.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from collections import defaultdict, deque
|
||||
from typing import Any, Deque, Dict, List
|
||||
|
||||
import requests
|
||||
|
||||
MISS = (
|
||||
"Hermes chưa trả lời được lúc này. Bạn thử lại sau hoặc diễn đạt lại câu hỏi nhé."
|
||||
)
|
||||
|
||||
# chat_id -> deque of {role, content} — maxlen set on first use / refresh
|
||||
_HISTORIES: Dict[str, Deque[dict[str, str]]] = {}
|
||||
|
||||
|
||||
def _cfg() -> dict[str, Any]:
|
||||
turns = int(os.environ.get("HERMES_HISTORY_TURNS", "12"))
|
||||
return {
|
||||
"base": os.environ.get("HERMES_API_BASE", "http://127.0.0.1:8642/v1").rstrip("/"),
|
||||
"key": (os.environ.get("HERMES_API_KEY") or "").strip(),
|
||||
"model": os.environ.get("HERMES_MODEL", "hermes-agent"),
|
||||
"timeout": float(os.environ.get("HERMES_TIMEOUT", "180")),
|
||||
"turns": turns,
|
||||
"backend": (os.environ.get("HERMES_BACKEND") or "api").strip().lower(),
|
||||
"system": (
|
||||
os.environ.get("HERMES_SYSTEM_PROMPT")
|
||||
or (
|
||||
"Bạn đang chat với người dùng qua Zalo Bot. "
|
||||
"Trả lời ngắn gọn, tiếng Việt trừ khi họ hỏi tiếng khác. "
|
||||
"Không dùng markdown nặng; ưu tiên plain text dễ đọc trên Zalo."
|
||||
)
|
||||
).strip(),
|
||||
}
|
||||
|
||||
|
||||
# Compat exports used by bot_app logging
|
||||
def _refresh_exports() -> None:
|
||||
global HERMES_API_BASE, HERMES_BACKEND
|
||||
c = _cfg()
|
||||
HERMES_API_BASE = c["base"]
|
||||
HERMES_BACKEND = c["backend"]
|
||||
|
||||
|
||||
_refresh_exports()
|
||||
|
||||
|
||||
def _auth_headers(key: str) -> dict[str, str]:
|
||||
h = {"Content-Type": "application/json", "User-Agent": "zalo-hermes-bot/0.1"}
|
||||
if key:
|
||||
h["Authorization"] = f"Bearer {key}"
|
||||
return h
|
||||
|
||||
|
||||
def _history_deque(chat_id: str, turns: int) -> Deque[dict[str, str]]:
|
||||
key = str(chat_id)
|
||||
maxlen = max(2, turns * 2)
|
||||
d = _HISTORIES.get(key)
|
||||
if d is None or d.maxlen != maxlen:
|
||||
old = list(d) if d is not None else []
|
||||
d = deque(old, maxlen=maxlen)
|
||||
_HISTORIES[key] = d
|
||||
return d
|
||||
|
||||
|
||||
def hermes_health() -> dict[str, Any]:
|
||||
c = _cfg()
|
||||
_refresh_exports()
|
||||
if c["backend"] == "cli":
|
||||
try:
|
||||
p = subprocess.run(
|
||||
["hermes", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
return {
|
||||
"ok": p.returncode == 0,
|
||||
"backend": "cli",
|
||||
"body": (p.stdout or p.stderr or "")[:200],
|
||||
}
|
||||
except Exception as e:
|
||||
return {"ok": False, "backend": "cli", "error": str(e)}
|
||||
|
||||
candidates = [
|
||||
f"{c['base']}/models",
|
||||
c["base"].replace("/v1", "") + "/health",
|
||||
"http://127.0.0.1:8642/health",
|
||||
]
|
||||
last: dict[str, Any] = {"ok": False, "backend": "api"}
|
||||
for url in candidates:
|
||||
try:
|
||||
r = requests.get(url, timeout=5, headers=_auth_headers(c["key"]))
|
||||
ctype = r.headers.get("content-type", "")
|
||||
body: Any = r.json() if "json" in ctype else r.text[:200]
|
||||
last = {
|
||||
"ok": r.status_code < 400,
|
||||
"backend": "api",
|
||||
"url": url,
|
||||
"status_code": r.status_code,
|
||||
"body": body,
|
||||
"api_key_len": len(c["key"]),
|
||||
}
|
||||
if last["ok"]:
|
||||
return last
|
||||
except Exception as e:
|
||||
last = {"ok": False, "backend": "api", "url": url, "error": str(e)}
|
||||
return last
|
||||
|
||||
|
||||
def _history_for(chat_id: str, turns: int) -> List[dict[str, str]]:
|
||||
return list(_history_deque(chat_id, turns))
|
||||
|
||||
|
||||
def _remember(chat_id: str, role: str, content: str, turns: int) -> None:
|
||||
_history_deque(chat_id, turns).append({"role": role, "content": content})
|
||||
|
||||
|
||||
def clear_history(chat_id: str) -> None:
|
||||
_HISTORIES.pop(str(chat_id), None)
|
||||
|
||||
|
||||
def ask_hermes_api(chat_id: str, message: str) -> dict[str, Any]:
|
||||
c = _cfg()
|
||||
msg = (message or "").strip()
|
||||
if not msg:
|
||||
return {"answer": MISS, "meta": "empty"}
|
||||
if not c["key"]:
|
||||
print("HERMES CLIENT: HERMES_API_KEY empty — load_dotenv before import, or export env")
|
||||
return {"answer": MISS, "meta": "missing api key"}
|
||||
|
||||
messages: list[dict[str, str]] = []
|
||||
if c["system"]:
|
||||
messages.append({"role": "system", "content": c["system"]})
|
||||
messages.extend(_history_for(chat_id, c["turns"]))
|
||||
messages.append({"role": "user", "content": msg})
|
||||
|
||||
url = f"{c['base']}/chat/completions"
|
||||
try:
|
||||
r = requests.post(
|
||||
url,
|
||||
json={
|
||||
"model": c["model"],
|
||||
"messages": messages,
|
||||
"stream": False,
|
||||
},
|
||||
timeout=c["timeout"],
|
||||
headers=_auth_headers(c["key"]),
|
||||
)
|
||||
if r.status_code == 401:
|
||||
print(
|
||||
"HERMES CLIENT: unauthorized — "
|
||||
f"api_key_len={len(c['key'])} (must match API_SERVER_KEY)"
|
||||
)
|
||||
return {"answer": MISS, "meta": "unauthorized"}
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
choices = data.get("choices") or []
|
||||
content = ""
|
||||
if choices:
|
||||
content = ((choices[0].get("message") or {}).get("content") or "").strip()
|
||||
if not content:
|
||||
return {"answer": MISS, "meta": "empty response"}
|
||||
_remember(chat_id, "user", msg, c["turns"])
|
||||
_remember(chat_id, "assistant", content, c["turns"])
|
||||
return {"answer": content, "meta": "api"}
|
||||
except Exception as e:
|
||||
print(f"HERMES CLIENT ERROR: {e}")
|
||||
return {"answer": MISS, "meta": f"error: {e}"}
|
||||
|
||||
|
||||
def ask_hermes_cli(message: str) -> dict[str, Any]:
|
||||
c = _cfg()
|
||||
msg = (message or "").strip()
|
||||
if not msg:
|
||||
return {"answer": MISS, "meta": "empty"}
|
||||
try:
|
||||
p = subprocess.run(
|
||||
["hermes", "chat", "-q", msg],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=c["timeout"],
|
||||
)
|
||||
out = (p.stdout or "").strip() or (p.stderr or "").strip()
|
||||
if p.returncode != 0 or not out:
|
||||
print(f"HERMES CLI fail rc={p.returncode}: {out[:300]}")
|
||||
return {"answer": MISS, "meta": f"cli rc={p.returncode}"}
|
||||
return {"answer": out, "meta": "cli"}
|
||||
except Exception as e:
|
||||
print(f"HERMES CLI ERROR: {e}")
|
||||
return {"answer": MISS, "meta": f"cli error: {e}"}
|
||||
|
||||
|
||||
def ask_hermes(chat_id: str, message: str) -> dict[str, Any]:
|
||||
c = _cfg()
|
||||
if c["backend"] == "cli":
|
||||
return ask_hermes_cli(message)
|
||||
return ask_hermes_api(chat_id, message)
|
||||
Reference in new issue
Block a user