Spaces:
Running
Running
Z User commited on
Commit ·
c310c5b
0
Parent(s):
Hermes Bot: initial deployment with Feishu gateway
Browse files- Dockerfile +45 -0
- README.md +17 -0
- SOUL.md +20 -0
- config.yaml +21 -0
- entry.py +115 -0
Dockerfile
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
# System deps
|
| 4 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 5 |
+
git curl gnupg2 \
|
| 6 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 7 |
+
|
| 8 |
+
# Create persistent data directories
|
| 9 |
+
RUN mkdir -p /data/hermes/{sessions,memories,uploads,logs,palace,skills}
|
| 10 |
+
|
| 11 |
+
WORKDIR /app
|
| 12 |
+
|
| 13 |
+
# Clone hermes-agent
|
| 14 |
+
RUN git clone --depth 1 https://github.com/NousResearch/hermes-agent.git /app/hermes-agent
|
| 15 |
+
|
| 16 |
+
# Build venv
|
| 17 |
+
RUN python3 -m venv /app/venv
|
| 18 |
+
ENV PATH="/app/venv/bin:$PATH"
|
| 19 |
+
RUN pip install --quiet --upgrade pip && \
|
| 20 |
+
pip install --quiet -e "/app/hermes-agent[feishu,mcp,cron,pty]" 2>&1 | tail -5
|
| 21 |
+
|
| 22 |
+
# Create hermes home with symlinks to persistent storage
|
| 23 |
+
RUN mkdir -p /root/.hermes && \
|
| 24 |
+
ln -sf /data/hermes/sessions /root/.hermes/sessions && \
|
| 25 |
+
ln -sf /data/hermes/memories /root/.hermes/memories && \
|
| 26 |
+
ln -sf /data/hermes/uploads /root/.hermes/uploads && \
|
| 27 |
+
ln -sf /data/hermes/logs /root/.hermes/logs && \
|
| 28 |
+
ln -sf /data/hermes/palace /root/.hermes/palace && \
|
| 29 |
+
ln -sf /data/hermes/skills /root/.hermes/skills
|
| 30 |
+
|
| 31 |
+
# Copy config files
|
| 32 |
+
COPY config.yaml /root/.hermes/config.yaml
|
| 33 |
+
COPY SOUL.md /root/.hermes/SOUL.md
|
| 34 |
+
COPY .env /root/.hermes/.env
|
| 35 |
+
COPY entry.py /app/entry.py
|
| 36 |
+
|
| 37 |
+
RUN chmod 600 /root/.hermes/.env
|
| 38 |
+
|
| 39 |
+
# Expose port (for HF health check, not actually serving HTTP)
|
| 40 |
+
EXPOSE 7860
|
| 41 |
+
|
| 42 |
+
ENV HERMES_ACCEPT_HOOKS=1
|
| 43 |
+
ENV MEMPALACE_PALACE_PATH=/data/hermes/palace
|
| 44 |
+
|
| 45 |
+
CMD ["python3", "/app/entry.py"]
|
README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Hermes Bot
|
| 3 |
+
emoji: ⚕️
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Hermes Agent — Feishu Bot
|
| 12 |
+
|
| 13 |
+
AI assistant powered by [Hermes Agent](https://github.com/NousResearch/hermes-agent) with Feishu integration.
|
| 14 |
+
|
| 15 |
+
**Model:** inclusionai/ling-2.6-1t:free (OpenRouter)
|
| 16 |
+
|
| 17 |
+
> ⚠️ This Space uses Persistent Storage for session/memory persistence.
|
SOUL.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are Hermes Agent, an intelligent AI assistant created by Nous Research. You are helpful, knowledgeable, and direct. You assist users with a wide range of tasks including answering questions, writing and editing code, analyzing information, creative work, and executing actions via your tools. You communicate clearly, admit uncertainty when appropriate, and prioritize being genuinely useful over being verbose.
|
| 2 |
+
|
| 3 |
+
## Memory Iron Rules (Must follow every conversation turn)
|
| 4 |
+
|
| 5 |
+
### Rule 1: Load memory at conversation start
|
| 6 |
+
- When receiving a user message, first check memory tools for user-related info
|
| 7 |
+
- Review previous conversation context and user preferences
|
| 8 |
+
|
| 9 |
+
### Rule 2: Save key information immediately
|
| 10 |
+
- User's personal info (name, preferences, projects, etc.) -> save to memory immediately
|
| 11 |
+
- Important conversation conclusions and decisions -> save immediately
|
| 12 |
+
- Don't wait until conversation ends to save
|
| 13 |
+
|
| 14 |
+
### Rule 3: Check for missed saves before ending
|
| 15 |
+
- Before the last reply, check if any important info hasn't been saved
|
| 16 |
+
- Ensure all key memories are persisted
|
| 17 |
+
|
| 18 |
+
### Rule 4: Memory usage norms
|
| 19 |
+
- Call memory-related tools at least once per conversation turn (read or write)
|
| 20 |
+
- Reference memory content naturally in replies, don't force "based on memory..."
|
config.yaml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model: inclusionai/ling-2.6-1t:free
|
| 2 |
+
provider: openrouter
|
| 3 |
+
max_turns: 90
|
| 4 |
+
platforms:
|
| 5 |
+
feishu:
|
| 6 |
+
enabled: true
|
| 7 |
+
extra:
|
| 8 |
+
domain: feishu
|
| 9 |
+
connection_mode: websocket
|
| 10 |
+
memory:
|
| 11 |
+
provider: none
|
| 12 |
+
compress:
|
| 13 |
+
enabled: true
|
| 14 |
+
threshold: 50
|
| 15 |
+
target_ratio: 20
|
| 16 |
+
protect_last: 20
|
| 17 |
+
no_mcp: true
|
| 18 |
+
terminal:
|
| 19 |
+
backend: local
|
| 20 |
+
timeout: 180
|
| 21 |
+
timezone: Asia/Shanghai
|
entry.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Hermes Agent entry point for HuggingFace Space.
|
| 3 |
+
|
| 4 |
+
- Starts a trivial HTTP server on port 7860 (HF health check).
|
| 5 |
+
- Launches the Hermes Gateway in a background thread.
|
| 6 |
+
- Keeps running until interrupted.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
import threading
|
| 12 |
+
import time
|
| 13 |
+
import logging
|
| 14 |
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
| 15 |
+
|
| 16 |
+
logging.basicConfig(
|
| 17 |
+
level=logging.INFO,
|
| 18 |
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
| 19 |
+
)
|
| 20 |
+
logger = logging.getLogger("entry")
|
| 21 |
+
|
| 22 |
+
# ---------------------------------------------------------------------------
|
| 23 |
+
# Health-check server (HF requires port 7860 to be listening)
|
| 24 |
+
# ---------------------------------------------------------------------------
|
| 25 |
+
|
| 26 |
+
class _HealthHandler(BaseHTTPRequestHandler):
|
| 27 |
+
def do_GET(self):
|
| 28 |
+
self.send_response(200)
|
| 29 |
+
self.send_header("Content-Type", "text/plain")
|
| 30 |
+
self.end_headers()
|
| 31 |
+
self.wfile.write(b"Hermes Gateway is running")
|
| 32 |
+
|
| 33 |
+
def log_message(self, fmt, *args):
|
| 34 |
+
pass # silence health-check logs
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _start_health_server(port=7860):
|
| 38 |
+
server = HTTPServer(("0.0.0.0", port), _HealthHandler)
|
| 39 |
+
logger.info("Health-check server listening on :%d", port)
|
| 40 |
+
server.serve_forever()
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
# ---------------------------------------------------------------------------
|
| 44 |
+
# Memory health check
|
| 45 |
+
# ---------------------------------------------------------------------------
|
| 46 |
+
|
| 47 |
+
def check_memory_health():
|
| 48 |
+
"""Verify persistent storage symlinks are intact."""
|
| 49 |
+
hermes_home = os.path.expanduser("~/.hermes")
|
| 50 |
+
issues = []
|
| 51 |
+
for subdir in ("sessions", "memories", "uploads"):
|
| 52 |
+
target = os.path.join(hermes_home, subdir)
|
| 53 |
+
if os.path.islink(target):
|
| 54 |
+
real = os.path.realpath(target)
|
| 55 |
+
if not os.path.isdir(real):
|
| 56 |
+
issues.append(f"{subdir} -> {real} (target missing)")
|
| 57 |
+
else:
|
| 58 |
+
logger.info("Symlink OK: %s -> %s", subdir, real)
|
| 59 |
+
elif os.path.isdir(target):
|
| 60 |
+
logger.warning("NOT a symlink: %s (data may be lost on rebuild)", target)
|
| 61 |
+
else:
|
| 62 |
+
issues.append(f"{subdir} missing")
|
| 63 |
+
|
| 64 |
+
# Check persistent volume writable
|
| 65 |
+
test_path = "/data/hermes/logs/write_test"
|
| 66 |
+
try:
|
| 67 |
+
os.makedirs(os.path.dirname(test_path), exist_ok=True)
|
| 68 |
+
with open(test_path, "w") as f:
|
| 69 |
+
f.write("ok")
|
| 70 |
+
os.remove(test_path)
|
| 71 |
+
logger.info("Persistent storage /data/hermes is writable")
|
| 72 |
+
except Exception as e:
|
| 73 |
+
issues.append(f"Cannot write to /data/hermes: {e}")
|
| 74 |
+
|
| 75 |
+
if issues:
|
| 76 |
+
logger.warning("Memory health issues: %s", "; ".join(issues))
|
| 77 |
+
else:
|
| 78 |
+
logger.info("Memory health check PASSED")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# ---------------------------------------------------------------------------
|
| 82 |
+
# Main
|
| 83 |
+
# ---------------------------------------------------------------------------
|
| 84 |
+
|
| 85 |
+
def main():
|
| 86 |
+
logger.info("=== Hermes Agent — HuggingFace Space Entry ===")
|
| 87 |
+
|
| 88 |
+
# Check persistent storage
|
| 89 |
+
check_memory_health()
|
| 90 |
+
|
| 91 |
+
# Start health-check server in background
|
| 92 |
+
health_thread = threading.Thread(target=_start_health_server, daemon=True)
|
| 93 |
+
health_thread.start()
|
| 94 |
+
|
| 95 |
+
# Launch Hermes Gateway
|
| 96 |
+
from hermes_cli.main import cli
|
| 97 |
+
logger.info("Launching Hermes Gateway...")
|
| 98 |
+
|
| 99 |
+
# The gateway blocks, so we run it in the main thread
|
| 100 |
+
try:
|
| 101 |
+
sys.argv = ["hermes", "gateway", "run", "-v"]
|
| 102 |
+
cli()
|
| 103 |
+
except KeyboardInterrupt:
|
| 104 |
+
logger.info("Shutting down...")
|
| 105 |
+
except SystemExit as e:
|
| 106 |
+
if e.code != 0:
|
| 107 |
+
logger.error("Gateway exited with code %s", e.code)
|
| 108 |
+
raise
|
| 109 |
+
except Exception as e:
|
| 110 |
+
logger.error("Gateway crashed: %s", e, exc_info=True)
|
| 111 |
+
raise
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
if __name__ == "__main__":
|
| 115 |
+
main()
|