hermes-mo / Dockerfile
heiyuheiyu's picture
Upload Dockerfile
871ebc3 verified
# =============================================================================
# Hermes Agent โ€” HuggingFace Space๏ผˆDocker๏ผ‰
# ไฝ ๅช้œ€ไธŠไผ ่ฟ™ไธ€ไธชๆ–‡ไปถ๏ผ
# =============================================================================
FROM python:3.11-slim
# โ”€โ”€ ็ณป็ปŸไพ่ต– โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
RUN apt-get update && apt-get install -y --no-install-recommends \
git curl wget build-essential libssl-dev libffi-dev \
nodejs npm procps \
&& rm -rf /var/lib/apt/lists/*
# โ”€โ”€ ๅฎ‰่ฃ… uv โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.cargo/bin:/root/.local/bin:$PATH"
# โ”€โ”€ ๅ…‹้š† hermes-agent โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
WORKDIR /opt
RUN git clone --depth 1 https://github.com/NousResearch/hermes-agent.git hermes-agent
WORKDIR /opt/hermes-agent
# โ”€โ”€ Python ่™šๆ‹Ÿ็Žฏๅขƒ + ไพ่ต– โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
RUN uv venv /opt/venv --python 3.11
ENV VIRTUAL_ENV=/opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# mini-swe-agent ๅทฒไฝœไธบ็›ฎๅฝ•ๅ†…ๅตŒๅœจไป“ๅบ“ไธญ๏ผˆไธๅ†ๆ˜ฏ git submodule๏ผ‰
# ่‹ฅ็›ฎๅฝ•ๅญ˜ๅœจๅˆ™ๅฎ‰่ฃ…๏ผŒๅฆๅˆ™่ทณ่ฟ‡
RUN uv pip install -e ".[all]" && \
([ -f "./mini-swe-agent/pyproject.toml" ] && uv pip install -e "./mini-swe-agent" || true) && \
uv pip install aiohttp httpx cryptography "gradio==5.29.0" "huggingface_hub>=0.23"
# โ”€โ”€ ๅ†™ๅ…ฅ entrypoint.sh โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
RUN cat > /entrypoint.sh << 'ENTRY_EOF'
#!/bin/bash
set -e
echo "=========================================================="
echo " Hermes Agent ยท HuggingFace Space"
echo "=========================================================="
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
# ๅ…จๅฑ€่ทฏๅพ„
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
export HERMES_HOME=/data/hermes
export HF_DATASET_LOCAL=/data/hf_dataset # Dataset ๆœฌๅœฐ้•œๅƒ
export HF_HUB_DISABLE_PROGRESS_BARS=1
mkdir -p "$HERMES_HOME"/{logs,cron}
mkdir -p "$HF_DATASET_LOCAL"/{skills,memories,sessions,logs,cron}
mkdir -p /data/workspace
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
# ๆญฅ้ชค 1: ๅˆๅง‹ๅŒ– HF Dataset๏ผˆๅฆ‚ไธๅญ˜ๅœจๅˆ™ๅˆ›ๅปบ๏ผ‰
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
echo "[1/6] ๅˆๅง‹ๅŒ– HuggingFace Dataset..."
python /dataset_manager.py init
echo "โœ… Dataset ๅˆๅง‹ๅŒ–ๅฎŒๆˆ"
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
# ๆญฅ้ชค 2: ไปŽ Dataset ๅ…‹้š†ๅˆฐๆœฌๅœฐ้•œๅƒ็›ฎๅฝ•๏ผˆๅฎžๆ—ถ่ฏปๅ†™ๅŸบ็ก€๏ผ‰
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
echo "[2/6] ไปŽ Dataset ๆ‹‰ๅ–้…็ฝฎ..."
python /dataset_manager.py pull
echo "โœ… ้…็ฝฎๅทฒๆ‹‰ๅ–"
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
# ๆญฅ้ชค 3: ๅฐ† HERMES_HOME ้‡Œ็š„็›ฎๅฝ•่ฝฏ้“พๅˆฐ Dataset ๆœฌๅœฐ้•œๅƒ
# ๅฎž็Žฐ"็›ดๆŽฅ่ฏปๅ†™ Dataset ๆ–‡ไปถ"ๆ•ˆๆžœ
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
echo "[3/6] ๅปบ็ซ‹็›ฎๅฝ•้“พๆŽฅ๏ผˆๅฎžๆ—ถ่ฏปๅ†™๏ผ‰..."
for DIR in config.yaml SOUL.md skills memories sessions cron; do
SRC="$HF_DATASET_LOCAL/$DIR"
DST="$HERMES_HOME/$DIR"
# ็กฎไฟ dataset ๆœฌๅœฐ้•œๅƒ้‡Œๆœ‰่ฟ™ไธช่ทฏๅพ„
if [[ "$DIR" == *.* ]]; then
# ๆ–‡ไปถ๏ผˆๅฆ‚ config.yaml, SOUL.md๏ผ‰
touch "$SRC" 2>/dev/null || true
else
mkdir -p "$SRC"
fi
# ๅˆ ้™คๆ—ง็š„ๅนถๅˆ›ๅปบ่ฝฏ้“พ
rm -rf "$DST"
ln -s "$SRC" "$DST"
echo " ๐Ÿ”— $DST -> $SRC"
done
echo "โœ… ็›ฎๅฝ•้“พๆŽฅๅฎŒๆˆ"
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
# ๆญฅ้ชค 4: ็”Ÿๆˆ .env๏ผˆไปŽ HF Space Secrets ๅ†™ๅ…ฅ๏ผ‰
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
echo "[4/6] ็”Ÿๆˆ .env..."
python /dataset_manager.py gen_env
echo "โœ… .env ๅทฒ็”Ÿๆˆ"
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
# ๆญฅ้ชค 5: ๅฏๅŠจ WeCom Gateway๏ผˆๅŽๅฐ๏ผ‰
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
echo "[5/6] ๅฏๅŠจ WeCom Gateway..."
if [ -n "${WECOM_BOT_ID}" ] && [ -n "${WECOM_SECRET}" ]; then
cd /opt/hermes-agent
HERMES_HOME=/data/hermes python -m hermes_cli.main gateway \
>> /data/hermes/logs/gateway.log 2>&1 &
echo "โœ… WeCom Gateway ๅทฒๅฏๅŠจ (PID: $!)"
else
echo "โš ๏ธ ๆœช้…็ฝฎ WECOM_BOT_ID / WECOM_SECRET๏ผŒ่ทณ่ฟ‡ WeCom Gateway"
fi
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
# ๆญฅ้ชค 6: ๅฏๅŠจ Dataset ๆ–‡ไปถ็›‘่ง†ๅ™จ๏ผˆๅฎžๆ—ถๆŽจ้€ๅ˜ๆ›ด + 10ๅˆ†้’ŸๆŽจ logs๏ผ‰
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
echo "[6/6] ๅฏๅŠจๆ–‡ไปถ็›‘่ง†ๅ™จ..."
python /dataset_manager.py watch &
echo "โœ… ๆ–‡ไปถ็›‘่ง†ๅ™จๅทฒๅฏๅŠจ"
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
# ๅฏๅŠจ Gradio Web UI๏ผˆๅ‰ๅฐ๏ผŒ็ซฏๅฃ 7860๏ผ‰
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
echo ""
echo "=========================================================="
echo " ๐ŸŒ ๅฏๅŠจ Gradio Web UI โ†’ http://0.0.0.0:7860"
echo "=========================================================="
exec python /webui.py
ENTRY_EOF
RUN chmod +x /entrypoint.sh
# โ”€โ”€ ๅ†™ๅ…ฅ dataset_manager.py โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
RUN cat > /dataset_manager.py << 'DM_EOF'
"""
dataset_manager.py
็ปŸไธ€็ฎก็† HuggingFace Dataset ็š„ๅˆๅง‹ๅŒ–ใ€ๆ‹‰ๅ–ใ€ๆŽจ้€ใ€ๆ–‡ไปถ็›‘่ง†ใ€‚
ๅญๅ‘ฝไปค:
init โ€” ๅˆ›ๅปบ Dataset๏ผˆ่‹ฅไธๅญ˜ๅœจ๏ผ‰ๅนถๅ†™ๅ…ฅๅˆๅง‹ๆ–‡ไปถ
pull โ€” ไปŽ Dataset ๆ‹‰ๅ–ๆ‰€ๆœ‰ๆ–‡ไปถๅˆฐๆœฌๅœฐ้•œๅƒ
push โ€” ๅฐ†ๆœฌๅœฐ้•œๅƒๆŽจ้€ๅˆฐ Dataset
gen_env โ€” ไปŽ็Žฏๅขƒๅ˜้‡็”Ÿๆˆ HERMES_HOME/.env
watch โ€” ็›‘่ง†ๆ–‡ไปถๅ˜ๅŒ–๏ผŒๅฎžๆ—ถๆŽจ้€๏ผ›logs ๆฏ10ๅˆ†้’ŸๆŽจไธ€ๆฌก
"""
import os, sys, time, hashlib, shutil
from pathlib import Path
from datetime import datetime
# โ”€โ”€ ้…็ฝฎ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
HF_TOKEN = os.environ.get("HF_TOKEN", "")
HF_DATASET_REPO = os.environ.get("HF_DATASET_REPO", "")
LOCAL_MIRROR = Path(os.environ.get("HF_DATASET_LOCAL", "/data/hf_dataset"))
HERMES_HOME = Path(os.environ.get("HERMES_HOME", "/data/hermes"))
# ๅฎžๆ—ถๅŒๅ‘ๅŒๆญฅ็š„่ทฏๅพ„๏ผˆ็›ธๅฏนไบŽ LOCAL_MIRROR / HERMES_HOME๏ผ‰
REALTIME_PATHS = ["config.yaml", "SOUL.md", "skills", "memories", "sessions", "cron"]
# ไป…ๅฎšๆ—ถๆŽจ้€๏ผˆไธๆ‹‰ๅ–๏ผ‰
LOG_ONLY_PATHS = ["logs"]
LOG_PUSH_INTERVAL = 600 # 10 ๅˆ†้’ŸๆŽจไธ€ๆฌก logs
# โ”€โ”€ ้ป˜่ฎคๅˆๅง‹ๆ–‡ไปถ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def _build_default_config_yaml():
"""
ๅŠจๆ€็”Ÿๆˆ้ฆ–ๆฌก้ƒจ็ฝฒๆ—ถๅ†™ๅ…ฅ Dataset ็š„ config.yamlใ€‚
- ๆ‰€ๆœ‰ๆœ‰ๆ•ˆ Provider ๅ†™ๅ…ฅ custom_providers: ๅ—๏ผˆname/url/api_key/models๏ผ‰
- model: ๅ—ๅชๅ†™ provider๏ผˆๆŒ‡ๅ‘ custom_providers ้‡Œ็š„ name๏ผ‰ๅ’Œ default ๆจกๅž‹
- ็•™็ฉบ็š„ Provider / Model ่ทณ่ฟ‡ไธๅ†™
- ไป…ๅœจ Dataset ้‡Œไธๅญ˜ๅœจ config.yaml ๆ—ถๆ‰ๅ†™ๅ…ฅ๏ผŒๆœ‰ๅค‡ไปฝๅˆ™ไธ่ฆ†็›–
"""
import os as _os
# ๆ”ถ้›†ๆ‰€ๆœ‰ๆœ‰ๆ•ˆ Provider
providers = []
for p in range(1, 6):
name = _os.environ.get(f"PROVIDER_{p}_NAME", "").strip()
url = _os.environ.get(f"PROVIDER_{p}_BASE_URL", "").strip()
api_key = _os.environ.get(f"PROVIDER_{p}_API_KEY", "").strip()
models = []
for m in range(1, 11):
mdl = _os.environ.get(f"PROVIDER_{p}_MODEL_{m}", "").strip()
if mdl:
models.append(mdl)
if name and url and api_key:
providers.append({"name": name, "url": url, "api_key": api_key, "models": models})
# model: ๅ— โ€”โ€” ๅชๅผ•็”จ custom_providers ้‡Œ็š„ name๏ผŒไธ้‡ๅคๅ†™ๅ‡ญ่ฏ
if providers:
p0 = providers[0]
p0_model = p0["models"][0] if p0["models"] else ""
model_block = (
"# -- Hermes ไธปๆจกๅž‹๏ผˆๅผ•็”จไธ‹ๆ–น custom_providers ไธญ็š„็ฌฌไธ€ไธช Provider๏ผ‰\n"
"model:\n"
f" provider: {p0['name']}\n"
f" default: {p0_model}\n"
)
else:
model_block = (
"# -- ไธปๆจกๅž‹๏ผˆๆœชๆฃ€ๆต‹ๅˆฐ PROVIDER_1_* ็Žฏๅขƒๅ˜้‡๏ผŒ่ฏทๆ‰‹ๅŠจ้…็ฝฎ๏ผ‰\n"
"# model:\n"
"# provider: my-provider-name\n"
"# default: google/gemma-4-31b-it\n"
)
# custom_providers: ๅ— โ€”โ€” ้›†ไธญ็ฎก็†ๆ‰€ๆœ‰ Provider ็š„ๅ‡ญ่ฏๅ’Œๆจกๅž‹ๅˆ—่กจ
if providers:
cp_lines = [
"# -- ่‡ชๅฎšไน‰ Provider ๅˆ—่กจ๏ผˆๅ‡ญ่ฏ้›†ไธญๅœจๆญค๏ผŒmodel: ๅ—้€š่ฟ‡ name ๅผ•็”จ๏ผ‰\n",
"custom_providers:\n",
]
for pv in providers:
cp_lines.append(f" - name: {pv['name']}\n")
cp_lines.append(f" base_url: {pv['url']}\n")
cp_lines.append(f" api_key: {pv['api_key']}\n")
if pv["models"]:
cp_lines.append(" models:\n")
for mdl in pv["models"]:
cp_lines.append(f" {mdl}:\n")
cp_lines.append( " context_length: 131072\n")
else:
cp_lines = ["# custom_providers: []\n"]
lines = [
"# =============================================================\n",
"# Hermes Agent config.yaml\n",
"# ็›ดๆŽฅๅœจ HF Dataset ้‡Œ็ผ–่พ‘ๆญคๆ–‡ไปถ๏ผŒไฟๅญ˜ๅŽ็ซ‹ๅณ็”Ÿๆ•ˆ๏ผˆๆ— ้œ€้‡ๅฏ๏ผ‰\n",
"# Provider ้…็ฝฎ็”ฑ HF Space ็š„ PROVIDER_{1-5}_* ็Žฏๅขƒๅ˜้‡่‡ชๅŠจๆณจๅ…ฅ\n",
"# ๆœ‰ๅค‡ไปฝๆ—ถไธไผš่ฆ†็›–๏ผŒๅˆ ๆމ Dataset ้‡Œ็š„ config.yaml ๅŽ้‡ๅฏๅณๅฏ้‡ๆ–ฐ็”Ÿๆˆ\n",
"# =============================================================\n",
"\n",
model_block,
"\n",
"".join(cp_lines),
"\n",
"# -- ็ปˆ็ซฏๅŽ็ซฏ๏ผˆHF Space ็”จ local๏ผ‰\n",
"terminal:\n",
" backend: local\n",
" cwd: /data/workspace\n",
" timeout: 180\n",
"\n",
"# -- ่ฎฐๅฟ†\n",
"memory:\n",
" memory_enabled: true\n",
" user_profile_enabled: true\n",
" memory_char_limit: 2200\n",
" user_char_limit: 1375\n",
"\n",
"# -- ๆ˜พ็คบ\n",
"display:\n",
" tool_progress: all\n",
" streaming: false\n",
"\n",
"# -- ๅŽ‹็ผฉ\n",
"compression:\n",
" enabled: true\n",
" threshold: 0.50\n",
"\n",
"# -- ๆ—ถๅŒบ\n",
'timezone: "Asia/Shanghai"\n',
"\n",
"# -- ๅฎ‰ๅ…จ๏ผˆๆ— ไบคไบ’็ปˆ็ซฏ๏ผŒๅ…ณ้—ญๅฎกๆ‰น๏ผ‰\n",
"approvals:\n",
" mode: off\n",
"\n",
"# -- WeCom๏ผˆไผไธšๅพฎไฟก๏ผ‰\n",
"platforms:\n",
" wecom:\n",
" enabled: true\n",
" extra:\n",
' bot_id: "${WECOM_BOT_ID}"\n',
' secret: "${WECOM_SECRET}"\n',
" dm_policy: open\n",
" group_policy: open\n",
]
return "".join(lines)
DEFAULT_CONFIG_YAML = _build_default_config_yaml()
DEFAULT_SOUL_MD = """\
# Hermes Agent
ไฝ ๆ˜ฏ Hermes๏ผŒไธ€ไธช็”ฑ Nous Research ๅผ€ๅ‘็š„ๆ™บ่ƒฝ AI ๅŠฉๆ‰‹ใ€‚
ไฝ ่ชๆ˜Žใ€ๅ‹ๅ–„ใ€ไนไบŽๅŠฉไบบ๏ผŒๅ…ทๅค‡ๆŒ็ปญๅญฆไน ๅ’Œ่‡ชๆˆ‘ๆ”น่ฟ›็š„่ƒฝๅŠ›ใ€‚
ไฝ ๆ“…้•ฟ๏ผš
- ๅ›ž็ญ”ๅ„็ฑป้—ฎ้ข˜ๅ’Œๆไพ›ๅปบ่ฎฎ
- ๆ‰ง่กŒไปฃ็ ๅ’Œๅˆ†ๆžๆ•ฐๆฎ
- ้•ฟๆœŸ่ฎฐๅฟ†็”จๆˆทๅๅฅฝๅ’ŒไธŠไธ‹ๆ–‡
- ้€š่ฟ‡ๅญฆไน ไธๆ–ญๅˆ›้€ ๅ’Œๆ”น่ฟ›ๆŠ€่ƒฝ
่ฏทๅง‹็ปˆ็”จๆธ…ๆ™ฐใ€ๅ‡†็กฎใ€ๆœ‰ๅธฎๅŠฉ็š„ๆ–นๅผๅ›žๅบ”็”จๆˆทใ€‚
"""
DEFAULT_MEMORY_MD = """\
# Agent ่ฎฐๅฟ†
๏ผˆๆš‚ๆ— ่ฎฐๅฟ†๏ผŒAgent ่ฟ่กŒๅŽไผš่‡ชๅŠจๅœจๆญค่ฎฐๅฝ•้‡่ฆไฟกๆฏ๏ผ‰
"""
DEFAULT_DATASET_README = """\
---
license: mit
tags:
- hermes-agent
- config
---
# Hermes Agent ้…็ฝฎๆ•ฐๆฎ้›†
ๆœฌ Dataset ็”ฑ Hermes Agent HF Space ่‡ชๅŠจๅˆ›ๅปบๅ’Œ็ฎก็†ใ€‚
## ็›ฎๅฝ•็ป“ๆž„
```
config.yaml โ† ไธป้…็ฝฎ๏ผˆ็›ดๆŽฅ็ผ–่พ‘ๅณๅฏ็”Ÿๆ•ˆ๏ผ‰
SOUL.md โ† Agent ไบบๆ ผๅฎšไน‰
skills/ โ† Agent ่‡ชๅˆ›ๆŠ€่ƒฝ
memories/ โ† ้•ฟๆœŸ่ฎฐๅฟ†
MEMORY.md
sessions/ โ† ไผš่ฏๅކๅฒ
cron/ โ† ๅฎšๆ—ถไปปๅŠก
logs/ โ† ่ฟ่กŒๆ—ฅๅฟ—๏ผˆ10ๅˆ†้’ŸๅŒๆญฅไธ€ๆฌก๏ผ‰
```
"""
# โ”€โ”€ ๅทฅๅ…ทๅ‡ฝๆ•ฐ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def get_api():
if not HF_TOKEN or not HF_DATASET_REPO:
print("โš ๏ธ HF_TOKEN ๆˆ– HF_DATASET_REPO ๆœช่ฎพ็ฝฎ๏ผŒ่ทณ่ฟ‡ Dataset ๆ“ไฝœ")
return None
try:
from huggingface_hub import HfApi
return HfApi(token=HF_TOKEN)
except ImportError:
print("โŒ huggingface_hub ๆœชๅฎ‰่ฃ…")
return None
def file_hash(path: Path) -> str:
if not path.exists() or not path.is_file():
return ""
return hashlib.md5(path.read_bytes()).hexdigest()
def upload_file(api, local_path: Path, repo_path: str, msg: str = "auto"):
try:
api.upload_file(
path_or_fileobj=str(local_path),
path_in_repo=repo_path,
repo_id=HF_DATASET_REPO,
repo_type="dataset",
commit_message=msg,
)
return True
except Exception as e:
print(f" โš ๏ธ ไธŠไผ  {repo_path} ๅคฑ่ดฅ: {e}")
return False
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
def cmd_init():
"""ๅˆ›ๅปบ Dataset๏ผˆ่‹ฅไธๅญ˜ๅœจ๏ผ‰ๅนถๅ†™ๅ…ฅๅˆๅง‹ๆ–‡ไปถ"""
api = get_api()
if not api:
_write_defaults_locally()
return
# ๆฃ€ๆŸฅ Dataset ๆ˜ฏๅฆๅญ˜ๅœจ
try:
from huggingface_hub import DatasetCard
api.dataset_info(HF_DATASET_REPO)
print(f" Dataset [{HF_DATASET_REPO}] ๅทฒๅญ˜ๅœจ")
except Exception:
print(f" Dataset [{HF_DATASET_REPO}] ไธๅญ˜ๅœจ๏ผŒๆญฃๅœจๅˆ›ๅปบ...")
api.create_repo(
repo_id=HF_DATASET_REPO,
repo_type="dataset",
private=True,
exist_ok=True,
)
print(f" โœ… Dataset ๅทฒๅˆ›ๅปบ๏ผˆ็งๆœ‰๏ผ‰")
# ๆฃ€ๆŸฅๅนถๅ†™ๅ…ฅๅˆๅง‹ๆ–‡ไปถ๏ผˆไธ่ฆ†็›–ๅทฒๆœ‰ๆ–‡ไปถ๏ผ‰
_init_remote_files(api)
# ๅ†™ๅˆฐๆœฌๅœฐ้•œๅƒ
_write_defaults_locally()
def _init_remote_files(api):
"""ๆฃ€ๆŸฅ่ฟœ็จ‹ๆ˜ฏๅฆๆœ‰ๅˆๅง‹ๆ–‡ไปถ๏ผŒๆฒกๆœ‰ๅˆ™ไธŠไผ ้ป˜่ฎคๅ€ผ"""
defaults = {
"README.md": DEFAULT_DATASET_README,
"config.yaml": DEFAULT_CONFIG_YAML,
"SOUL.md": DEFAULT_SOUL_MD,
"memories/MEMORY.md": DEFAULT_MEMORY_MD,
}
try:
existing = {f.rfilename for f in api.list_repo_files(
HF_DATASET_REPO, repo_type="dataset")}
except Exception:
existing = set()
for repo_path, content in defaults.items():
if repo_path not in existing:
tmp = Path(f"/tmp/init_{repo_path.replace('/', '_')}")
tmp.parent.mkdir(parents=True, exist_ok=True)
tmp.write_text(content, encoding="utf-8")
upload_file(api, tmp, repo_path, "Init: " + repo_path)
print(f" ๐Ÿ“ ๅˆๅง‹ๅŒ–่ฟœ็จ‹ๆ–‡ไปถ: {repo_path}")
else:
print(f" โœ“ ๅทฒๅญ˜ๅœจ: {repo_path}")
def _write_defaults_locally():
"""็กฎไฟๆœฌๅœฐ้•œๅƒๆœ‰้ป˜่ฎคๆ–‡ไปถ"""
LOCAL_MIRROR.mkdir(parents=True, exist_ok=True)
defaults = {
"config.yaml": DEFAULT_CONFIG_YAML,
"SOUL.md": DEFAULT_SOUL_MD,
"memories/MEMORY.md": DEFAULT_MEMORY_MD,
}
for rel, content in defaults.items():
p = LOCAL_MIRROR / rel
if not p.exists():
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(content, encoding="utf-8")
for d in ["skills", "sessions", "cron", "logs"]:
(LOCAL_MIRROR / d).mkdir(parents=True, exist_ok=True)
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
def cmd_pull():
"""ไปŽ Dataset ๆ‹‰ๅ–ๆ‰€ๆœ‰ๆ–‡ไปถๅˆฐๆœฌๅœฐ้•œๅƒ๏ผˆๅฎžๆ—ถ่ทฏๅพ„๏ผ‰"""
api = get_api()
if not api:
return
LOCAL_MIRROR.mkdir(parents=True, exist_ok=True)
try:
from huggingface_hub import snapshot_download
snapshot_download(
repo_id=HF_DATASET_REPO,
repo_type="dataset",
token=HF_TOKEN,
local_dir=str(LOCAL_MIRROR),
local_dir_use_symlinks=False,
ignore_patterns=["*.git*", ".gitattributes", "README.md"],
)
print(f" โœ… ๅทฒไปŽ Dataset ๆ‹‰ๅ–ๅˆฐ {LOCAL_MIRROR}")
except Exception as e:
print(f" โš ๏ธ pull ๅคฑ่ดฅ: {e}๏ผŒไฝฟ็”จๆœฌๅœฐ้ป˜่ฎคๅ€ผ")
_write_defaults_locally()
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
def cmd_gen_env():
"""ไปŽ็Žฏๅขƒๅ˜้‡็”Ÿๆˆ HERMES_HOME/.env"""
env_path = HERMES_HOME / ".env"
env_path.parent.mkdir(parents=True, exist_ok=True)
lines = ["# ็”ฑ dataset_manager.py ่‡ชๅŠจ็”Ÿๆˆ๏ผŒๅ‹ฟๆ‰‹ๅŠจ็ผ–่พ‘\n"]
# WeCom
for k in ["WECOM_BOT_ID", "WECOM_SECRET"]:
lines.append(f"{k}={os.environ.get(k, '')}\n")
# โ”€โ”€ ็”จๆˆทๆŽˆๆƒ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ไผ˜ๅ…ˆไฝฟ็”จ็ฒพ็กฎ็™ฝๅๅ•๏ผˆWECOM_ALLOWED_USERS๏ผ‰๏ผŒๅ…ถๆฌกๅ…จๅผ€ๆ”พ๏ผˆGATEWAY_ALLOW_ALL_USERS๏ผ‰
# ไธค่€…้ƒฝๆœช้…็ฝฎๆ—ถ้ป˜่ฎคๅ…จๅผ€ๆ”พ๏ผŒ้ฟๅ…ๆ‰€ๆœ‰็”จๆˆท้ƒฝ่ขซๆ‹’็ปๅนถๆ”ถๅˆฐ้…ๅฏน็ 
wecom_allowed = os.environ.get("WECOM_ALLOWED_USERS", "").strip()
gateway_allow = os.environ.get("GATEWAY_ALLOW_ALL_USERS", "").strip()
if wecom_allowed:
# ็ฒพ็กฎ็™ฝๅๅ•ไผ˜ๅ…ˆ๏ผšๅชๅ…่ฎธๅˆ—ๅ‡บ็š„็”จๆˆท๏ผˆ้€—ๅทๅˆ†้š”ไผไธšๅพฎไฟก็”จๆˆทๅๆˆ–ID๏ผ‰
lines.append(f"WECOM_ALLOWED_USERS={wecom_allowed}\n")
lines.append("GATEWAY_ALLOW_ALL_USERS=false\n")
print(f" ๐Ÿ”’ WeCom ็™ฝๅๅ•ๅทฒ่ฎพ็ฝฎ: {wecom_allowed}")
elif gateway_allow.lower() in ("false", "0", "no"):
# ๆ˜พๅผๅ…ณ้—ญๅ…จๅผ€ๆ”พ โ€”โ€” ็”จๆˆท้œ€่ฆๆ‰‹ๅŠจ้…ๅฏน
lines.append("GATEWAY_ALLOW_ALL_USERS=false\n")
print(" โš ๏ธ GATEWAY_ALLOW_ALL_USERS=false๏ผŒๆœช้…ๅฏน็”จๆˆทๅฐ†่ขซๆ‹’็ป")
else:
# ้ป˜่ฎคๅ…จๅผ€ๆ”พ๏ผˆGATEWAY_ALLOW_ALL_USERS ไธบ็ฉบๆˆ– true ๆ—ถ๏ผ‰
lines.append("GATEWAY_ALLOW_ALL_USERS=true\n")
print(" โœ… GATEWAY_ALLOW_ALL_USERS=true๏ผˆๆ‰€ๆœ‰็”จๆˆทๅ‡ๅฏไฝฟ็”จ๏ผ‰")
# Providers 1-5
for p in range(1, 6):
for k in ["NAME", "BASE_URL", "API_KEY"] + [f"MODEL_{m}" for m in range(1, 11)]:
var = f"PROVIDER_{p}_{k}"
lines.append(f"{var}={os.environ.get(var, '')}\n")
# HF
lines.append(f"HF_TOKEN={HF_TOKEN}\n")
lines.append(f"HF_DATASET_REPO={HF_DATASET_REPO}\n")
env_path.write_text("".join(lines), encoding="utf-8")
print(f" โœ… .env ๅทฒๅ†™ๅ…ฅ {env_path}")
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
def cmd_watch():
"""
ๆ–‡ไปถ็›‘่ง†ๅ™จ๏ผš
- ๅฎžๆ—ถ่ทฏๅพ„๏ผˆconfig, SOUL, skills, memories, sessions, cron๏ผ‰๏ผš
ๆฃ€ๆต‹ๅˆฐๅ˜ๅŒ–็ซ‹ๅณไธŠไผ ๅˆฐ Dataset
- logs๏ผšๆฏ 10 ๅˆ†้’Ÿๆ‰น้‡ไธŠไผ ไธ€ๆฌก
"""
api = get_api()
if not api:
print("โš ๏ธ ๆ—  API๏ผŒ็›‘่ง†ๅ™จ้€€ๅ‡บ")
return
print(" ๐Ÿ‘๏ธ ๆ–‡ไปถ็›‘่ง†ๅ™จๅฏๅŠจ๏ผˆๅฎžๆ—ถๅŒๆญฅ + 10ๅˆ†้’ŸๆŽจ logs๏ผ‰")
# ็ปดๆŠคๆ–‡ไปถๅ“ˆๅธŒ่กจ
hashes: dict[str, str] = {}
last_log_push = time.time()
def collect_realtime_files():
files = {}
for rel in REALTIME_PATHS:
p = LOCAL_MIRROR / rel
if p.is_file():
files[str(p.relative_to(LOCAL_MIRROR))] = p
elif p.is_dir():
for f in p.rglob("*"):
if f.is_file():
files[str(f.relative_to(LOCAL_MIRROR))] = f
return files
def collect_log_files():
files = {}
for rel in LOG_ONLY_PATHS:
p = LOCAL_MIRROR / rel
if p.is_dir():
for f in p.rglob("*"):
if f.is_file():
files[str(f.relative_to(LOCAL_MIRROR))] = f
return files
# ๅˆๅง‹ๅ“ˆๅธŒๅฟซ็…ง
for rel, path in collect_realtime_files().items():
hashes[rel] = file_hash(path)
while True:
try:
# โ”€โ”€ ๅฎžๆ—ถๆฃ€ๆต‹ๅ˜ๅŒ– โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
current = collect_realtime_files()
changed = []
for rel, path in current.items():
h = file_hash(path)
if hashes.get(rel) != h:
hashes[rel] = h
changed.append((rel, path))
for rel, path in changed:
ts = datetime.now().strftime("%H:%M:%S")
ok = upload_file(api, path, rel,
f"Realtime sync [{ts}]: {rel}")
if ok:
print(f" โ˜๏ธ [{ts}] ๅทฒๆŽจ้€: {rel}")
# โ”€โ”€ ๆฏ 10 ๅˆ†้’ŸๆŽจ logs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if time.time() - last_log_push >= LOG_PUSH_INTERVAL:
log_files = collect_log_files()
if log_files:
ts = datetime.now().strftime("%Y-%m-%d %H:%M")
for rel, path in log_files.items():
upload_file(api, path, rel, f"Log sync [{ts}]")
print(f" ๐Ÿ“‹ [{ts}] logs ๅทฒๆŽจ้€ ({len(log_files)} ไธชๆ–‡ไปถ)")
last_log_push = time.time()
except Exception as e:
print(f" โš ๏ธ ็›‘่ง†ๅ™จๅผ‚ๅธธ: {e}")
time.sleep(5) # ๆฏ 5 ็ง’ๆฃ€ๆต‹ไธ€ๆฌกๅ˜ๅŒ–
# โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
if __name__ == "__main__":
cmd = sys.argv[1] if len(sys.argv) > 1 else ""
{"init": cmd_init, "pull": cmd_pull,
"gen_env": cmd_gen_env, "watch": cmd_watch}.get(cmd, lambda: print(
"็”จๆณ•: python dataset_manager.py [init|pull|gen_env|watch]"))()
DM_EOF
# โ”€โ”€ ๅ†™ๅ…ฅ webui.py โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
RUN cat > /webui.py << 'WEBUI_EOF'
"""
webui.py โ€” Hermes Agent Gradio Web UI
็ซฏๅฃ: 7860
"""
import os, sys, subprocess, time, shutil
from pathlib import Path
import gradio as gr
HERMES_HOME = Path(os.environ.get("HERMES_HOME", "/data/hermes"))
HF_DS_LOCAL = Path(os.environ.get("HF_DATASET_LOCAL", "/data/hf_dataset"))
HERMES_SRC = Path("/opt/hermes-agent")
sys.path.insert(0, str(HERMES_SRC))
# โ”€โ”€ ่พ…ๅŠฉๅ‡ฝๆ•ฐ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def get_models():
models = []
for p in range(1, 6):
name = os.environ.get(f"PROVIDER_{p}_NAME", "").strip()
url = os.environ.get(f"PROVIDER_{p}_BASE_URL", "").strip()
api_key = os.environ.get(f"PROVIDER_{p}_API_KEY", "").strip()
if not (name and url and api_key):
continue
for m in range(1, 11):
model = os.environ.get(f"PROVIDER_{p}_MODEL_{m}", "").strip()
if model:
models.append((p, model, f"[{name}] {model}"))
return models
def run_hermes(message, history, provider_idx, model_name):
env = os.environ.copy()
env["HERMES_HOME"] = str(HERMES_HOME)
env["OPENAI_BASE_URL"] = os.environ.get(f"PROVIDER_{provider_idx}_BASE_URL", "")
env["OPENAI_API_KEY"] = os.environ.get(f"PROVIDER_{provider_idx}_API_KEY", "")
ctx = []
for m in history[-6:]:
role = m.get("role","")
content = m.get("content","")
if role == "user" and content: ctx.append(f"User: {content}")
elif role == "assistant" and content: ctx.append(f"Assistant: {content}")
ctx.append(f"User: {message}")
prompt = "\n".join(ctx)
try:
r = subprocess.run(
[sys.executable, str(HERMES_SRC/"run_agent.py"),
"--once", "--model", model_name, "--message", prompt],
capture_output=True, text=True, timeout=120,
env=env, cwd=str(HERMES_SRC)
)
out = r.stdout.strip()
return out if out else (r.stderr.strip()[:400] or "(ๆ— ๅ›žๅค)")
except subprocess.TimeoutExpired:
return "โš ๏ธ ่ถ…ๆ—ถ๏ผˆ120s๏ผ‰๏ผŒ่ฏท้‡่ฏ•"
except Exception as e:
return f"โš ๏ธ ่ฐƒ็”จๅคฑ่ดฅ: {e}"
# โ”€โ”€ ่ฏปๅ†™ Dataset ๆœฌๅœฐ้•œๅƒๆ–‡ไปถ๏ผˆ่ฝฏ้“พ๏ผŒๅฎžๆ—ถ็”Ÿๆ•ˆ๏ผ‰โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def r(path: Path, default=""):
return path.read_text("utf-8") if path.exists() else default
def w(path: Path, content: str, label=""):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, "utf-8")
return f"โœ… {label or path.name} ๅทฒไฟๅญ˜๏ผˆๆ–‡ไปถ็›‘่ง†ๅ™จๅฐ†่‡ชๅŠจๅŒๆญฅๅˆฐ Dataset๏ผ‰"
cfg_path = HF_DS_LOCAL / "config.yaml"
soul_path = HF_DS_LOCAL / "SOUL.md"
memory_path = HF_DS_LOCAL / "memories" / "MEMORY.md"
log_path = HERMES_HOME / "logs" / "gateway.log"
def gw_status():
r2 = subprocess.run(["pgrep","-f","hermes.*gateway"],capture_output=True,text=True)
return "โœ… WeCom Gateway ่ฟ่กŒไธญ" if r2.returncode==0 else "โš ๏ธ WeCom Gateway ๆœช่ฟ่กŒ"
def restart_gw():
subprocess.run(["pkill","-f","hermes.*gateway"],capture_output=True)
time.sleep(2)
if not (os.environ.get("WECOM_BOT_ID") and os.environ.get("WECOM_SECRET")):
return "โš ๏ธ WECOM_BOT_ID / WECOM_SECRET ๆœช้…็ฝฎ"
env = os.environ.copy()
env["HERMES_HOME"] = str(HERMES_HOME)
log_path.parent.mkdir(parents=True, exist_ok=True)
with open(log_path,"a") as lf:
proc = subprocess.Popen(
[sys.executable,"-m","hermes_cli.main","gateway"],
env=env, cwd=str(HERMES_SRC), stdout=lf, stderr=lf)
time.sleep(3)
return f"โœ… Gateway ๅทฒ้‡ๅฏ (PID: {proc.pid})"
def manual_push():
r2 = subprocess.run([sys.executable,"/dataset_manager.py","push"],
capture_output=True,text=True,timeout=60)
return r2.stdout + r2.stderr
def manual_pull():
r2 = subprocess.run([sys.executable,"/dataset_manager.py","pull"],
capture_output=True,text=True,timeout=60)
return r2.stdout + r2.stderr
# โ”€โ”€ ๆž„ๅปบ UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
all_models = get_models()
model_labels = [m[2] for m in all_models] or ["(ๆœช้…็ฝฎๆจกๅž‹)"]
def chat_fn(msg, history, model_choice, sp):
if not msg.strip():
yield history, ""
return
idx = next((m[0] for m in all_models if m[2]==model_choice), 1)
mname = next((m[1] for m in all_models if m[2]==model_choice), model_choice)
if sp.strip():
soul_path.parent.mkdir(parents=True,exist_ok=True)
soul_path.write_text(sp,"utf-8")
yield history + [{"role":"user","content":msg},{"role":"assistant","content":"๐Ÿ”„ ๆ€่€ƒไธญ..."}], ""
reply = run_hermes(msg, history, idx, mname)
yield history + [{"role":"user","content":msg},{"role":"assistant","content":reply}], ""
with gr.Blocks(title="Hermes Agent", theme=gr.themes.Soft()) as demo:
gr.Markdown("# โ˜ค Hermes Agent\n> [Nous Research](https://nousresearch.com) ยท HuggingFace Space")
with gr.Tabs():
# โ”€โ”€ ่Šๅคฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with gr.Tab("๐Ÿ’ฌ ่Šๅคฉ"):
with gr.Row():
with gr.Column(scale=3):
chatbot = gr.Chatbot(height=500, show_copy_button=True, type="messages")
with gr.Row():
inp = gr.Textbox(placeholder="่พ“ๅ…ฅๆถˆๆฏโ€ฆ",show_label=False,lines=2,scale=5)
gr.Button("ๅ‘้€",variant="primary",scale=1).click(
chat_fn,[inp,chatbot,
gr.Dropdown(choices=model_labels,value=model_labels[0],label="ๆจกๅž‹",interactive=True),
gr.Textbox(value=lambda:r(soul_path,"ไฝ ๆ˜ฏไธ€ไธชๆœ‰็”จ็š„ๅŠฉๆ‰‹"),label="็ณป็ปŸๆ็คบ่ฏ",lines=6)],
[chatbot,inp])
gr.Button("๐Ÿ—‘๏ธ ๆธ…็ฉบ",size="sm").click(lambda:[],outputs=[chatbot])
with gr.Column(scale=1):
model_dd = gr.Dropdown(choices=model_labels,value=model_labels[0],label="้€‰ๆ‹ฉๆจกๅž‹")
soul_box = gr.Textbox(value=lambda:r(soul_path),label="็ณป็ปŸๆ็คบ่ฏ(SOUL.md)",lines=10,interactive=True)
save_soul = gr.Button("๐Ÿ’พ ไฟๅญ˜ SOUL.md",size="sm")
soul_st = gr.Textbox(interactive=False,lines=1,label="")
save_soul.click(lambda c: w(soul_path,c,"SOUL.md"), [soul_box],[soul_st])
inp.submit(chat_fn,[inp,chatbot,model_dd,soul_box],[chatbot,inp])
# โ”€โ”€ WeCom โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with gr.Tab("๐Ÿ“ฑ WeCom ไผไธšๅพฎไฟก"):
gr.Markdown("""
### WeCom AI Bot๏ผˆWebSocket ๆจกๅผ๏ผŒๆ— ้œ€ๅ…ฌ็ฝ‘็ซฏ็‚น๏ผ‰
**้…็ฝฎๆญฅ้ชค๏ผš**
1. [ไผไธšๅพฎไฟก็ฎก็†ๅŽๅฐ](https://work.weixin.qq.com/wework_admin/frame) โ†’ ๅบ”็”จ็ฎก็† โ†’ ๅˆ›ๅปบๅบ”็”จ โ†’ **AI Bot**
2. ๅคๅˆถ Bot ID ๅ’Œ Secret
3. ๅœจ HF Space Settings โ†’ **Secrets** ไธญๆทปๅŠ ๏ผš
- `WECOM_BOT_ID` = Bot ID
- `WECOM_SECRET` = Secret
4. ๏ผˆๅฏ้€‰๏ผ‰ๅœจ HF Space Settings โ†’ **Variables** ไธญๆทปๅŠ ็”จๆˆท็™ฝๅๅ•๏ผš
- `WECOM_ALLOWED_USERS` = ๅ…่ฎธไฝฟ็”จ็š„ไผไธšๅพฎไฟก็”จๆˆทๅ๏ผŒ้€—ๅทๅˆ†้š”๏ผŒๅฆ‚ `ๅผ ไธ‰,ๆŽๅ››`
- ไธๅกซๅˆ™้ป˜่ฎค**ๆ‰€ๆœ‰็”จๆˆท**ๅ‡ๅฏไฝฟ็”จ๏ผˆ`GATEWAY_ALLOW_ALL_USERS=true`๏ผ‰
- ่‹ฅ่ฎพไธบ `GATEWAY_ALLOW_ALL_USERS=false` ไธ”ไธๅกซ็™ฝๅๅ•๏ผŒๅˆ™ๆ‰€ๆœ‰็”จๆˆท้œ€ๆ‰‹ๅŠจ้…ๅฏน
5. ็‚นๅ‡ปไธ‹ๆ–น"้‡ๅฏ Gateway"
""")
with gr.Row():
gw_box = gr.Textbox(label="Gateway ็Šถๆ€",interactive=False,lines=2)
with gr.Column():
gr.Button("๐Ÿ” ๆฃ€ๆŸฅ็Šถๆ€").click(gw_status,outputs=[gw_box])
gr.Button("๐Ÿ”„ ้‡ๅฏ Gateway",variant="primary").click(restart_gw,outputs=[gw_box])
# โ”€โ”€ ่ฎฐๅฟ† โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with gr.Tab("๐Ÿง  ่ฎฐๅฟ†็ฎก็†"):
gr.Markdown("็›ดๆŽฅ็ผ–่พ‘ MEMORY.md๏ผŒไฟๅญ˜ๅŽๆ–‡ไปถ็›‘่ง†ๅ™จ่‡ชๅŠจๅŒๆญฅๅˆฐ Dataset๏ผˆๆ— ้œ€ๆ‰‹ๅŠจ Push๏ผ‰ใ€‚")
mem_box = gr.Textbox(value=lambda:r(memory_path),label="MEMORY.md",lines=20,interactive=True)
with gr.Row():
gr.Button("๐Ÿ’พ ไฟๅญ˜",variant="primary").click(
lambda c: w(memory_path,c,"MEMORY.md"),[mem_box],[gr.Textbox(label="",lines=1)])
gr.Button("๐Ÿ”„ ๅˆทๆ–ฐ").click(lambda:r(memory_path),outputs=[mem_box])
# โ”€โ”€ ้…็ฝฎ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with gr.Tab("โš™๏ธ ้…็ฝฎๆ–‡ไปถ"):
gr.Markdown("""
็›ดๆŽฅ็ผ–่พ‘ `config.yaml`๏ผŒไฟๅญ˜ๅŽๆ–‡ไปถ็›‘่ง†ๅ™จ**ๅฎžๆ—ถๅŒๆญฅ**ๅˆฐ HF Datasetใ€‚
**WeCom Gateway** ้‡ๅฏๅŽ่ฏปๅ–ๆ–ฐ้…็ฝฎ๏ผ›**Web UI ่Šๅคฉ** ๅˆทๆ–ฐ้กต้ขๅณ็”Ÿๆ•ˆใ€‚
""")
cfg_box = gr.Code(value=lambda:r(cfg_path),language="yaml",label="config.yaml",lines=30,interactive=True)
with gr.Row():
gr.Button("๐Ÿ’พ ไฟๅญ˜ config.yaml",variant="primary").click(
lambda c: w(cfg_path,c,"config.yaml"),[cfg_box],[gr.Textbox(label="",lines=1)])
gr.Button("๐Ÿ”„ ๅˆทๆ–ฐ").click(lambda:r(cfg_path),outputs=[cfg_box])
# โ”€โ”€ Dataset โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with gr.Tab("โ˜๏ธ Dataset ๅŒๆญฅ"):
gr.Markdown("""
**่‡ชๅŠจๅŒๆญฅ็ญ–็•ฅ๏ผš**
- `config.yaml` / `SOUL.md` / `skills` / `memories` / `sessions` / `cron` โ€” ๆ–‡ไปถๅ˜ๅŒ–ๆ—ถ**็ซ‹ๅณ**ๆŽจ้€ๅˆฐ Dataset
- `logs` โ€” ๆฏ **10 ๅˆ†้’Ÿ**ๆ‰น้‡ๆŽจ้€ไธ€ๆฌก
ๆ‰‹ๅŠจๆ“ไฝœไป…ๅœจ้œ€่ฆๆ—ถไฝฟ็”จ๏ผˆๅฆ‚ๅผบๅˆถ่ฆ†็›–ๆˆ–ๆ‹‰ๅ–ไป–ไบบไฟฎๆ”น๏ผ‰ใ€‚
""")
with gr.Row():
gr.Button("๐Ÿ“ฅ ๆ‰‹ๅŠจ Pull๏ผˆไปŽ Dataset ่ฆ†็›–ๆœฌๅœฐ๏ผ‰").click(manual_pull,outputs=[gr.Textbox(label="่พ“ๅ‡บ",lines=10)])
gr.Button("๐Ÿ“ค ๆ‰‹ๅŠจ Push๏ผˆ็ซ‹ๅณๆŽจ้€ๅ…จ้ƒจ๏ผ‰",variant="primary").click(manual_push,outputs=[gr.Textbox(label="่พ“ๅ‡บ",lines=10)])
# โ”€โ”€ ๆ—ฅๅฟ— โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with gr.Tab("๐Ÿ“‹ ๆ—ฅๅฟ—"):
log_box = gr.Textbox(value=lambda: "\n".join(
(log_path.read_text("utf-8",errors="replace").splitlines()[-80:]
if log_path.exists() else ["(ๆš‚ๆ— ๆ—ฅๅฟ—)"])),
label="gateway.log๏ผˆๆœ€ๅŽ80่กŒ๏ผ‰",lines=25,interactive=False)
gr.Button("๐Ÿ”„ ๅˆทๆ–ฐๆ—ฅๅฟ—").click(
lambda: "\n".join(
log_path.read_text("utf-8",errors="replace").splitlines()[-80:]
if log_path.exists() else ["(ๆš‚ๆ— ๆ—ฅๅฟ—)"]),
outputs=[log_box])
gr.Markdown("---\n**GitHub**: [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent) ยท MIT License")
if __name__ == "__main__":
demo.queue().launch(server_name="0.0.0.0", server_port=7860, show_error=True)
WEBUI_EOF
# โ”€โ”€ Space README๏ผˆHF Space card๏ผŒๅฟ…้กปๆœ‰๏ผ‰โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
RUN cat > /README_SPACE.md << 'README_EOF'
---
title: Hermes Agent
emoji: โ˜ค
colorFrom: purple
colorTo: blue
sdk: docker
pinned: false
license: mit
---
# โ˜ค Hermes Agent
> ่‡ชๆˆ้•ฟ AI Agent ยท [Nous Research](https://nousresearch.com) ยท [GitHub](https://github.com/NousResearch/hermes-agent)
**ๅช้œ€ไธŠไผ ่ฟ™ไธ€ไธช Dockerfile ๅณๅฏๅฎŒๆˆ้ƒจ็ฝฒใ€‚**
ๅŠŸ่ƒฝ๏ผš
- ๐ŸŒ Gradio Web UI ่Šๅคฉ็•Œ้ข
- ๐Ÿ“ฑ WeCom ไผไธšๅพฎไฟก AI Bot๏ผˆWebSocket๏ผŒๆ— ้œ€ๅ…ฌ็ฝ‘๏ผ‰
- โ˜๏ธ HF Dataset ๅฎžๆ—ถ้…็ฝฎๆŒไน…ๅŒ–
- ๐Ÿ”‘ ๆ”ฏๆŒไปปๆ„ OpenAI ๅ…ผๅฎน็š„็ฌฌไธ‰ๆ–นๅคงๆจกๅž‹
README_EOF
# โ”€โ”€ ๅˆ›ๅปบๅทฅไฝœ็›ฎๅฝ• โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
RUN mkdir -p /data/workspace /data/hermes/logs /data/hf_dataset
EXPOSE 7860
CMD ["/entrypoint.sh"]