Text Generation
PEFT
Chinese
English
preference-learning
qlora
agent
personalization
association-engine
Instructions to use feiertu/hermes-association-engine with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use feiertu/hermes-association-engine with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
| """Hermes CLI — click 壳.""" | |
| import json | |
| import os | |
| import signal | |
| import subprocess | |
| import sys | |
| import textwrap | |
| import time | |
| from pathlib import Path | |
| # Fix Unicode display on Windows (GBK terminal) | |
| if sys.platform == "win32": | |
| sys.stdout.reconfigure(encoding="utf-8", errors="replace") | |
| sys.stderr.reconfigure(encoding="utf-8", errors="replace") | |
| import click | |
| from hermes_core.querier import HermesClient | |
| from hermes_core.refiner import refine_scene | |
| from hermes_core.db import init_db, get_record, update_record_state, get_active_records | |
| from hermes_core.types import RecordState, HERMES_DATA_DIR | |
| from hermes_core.config import get_config, save_config, ensure_config_exists, HermesConfig, CONFIG_FILE | |
| # ═══════════════════════════════════════════════════════════════ | |
| # 工具函数 | |
| # ═══════════════════════════════════════════════════════════════ | |
| def _echo_json(data: dict) -> None: | |
| click.echo(json.dumps(data, ensure_ascii=False, indent=2)) | |
| def _check_model_cached() -> bool: | |
| """检查 embedding 模型是否已下载。""" | |
| from hermes_core.embedder import Embedder | |
| return Embedder.is_available() | |
| # ═══════════════════════════════════════════════════════════════ | |
| # 主命令组 | |
| # ═══════════════════════════════════════════════════════════════ | |
| def cli(): | |
| """Hermes 联想引擎 — Agent 偏好学习与共享.""" | |
| # ═══════════════════════════════════════════════════════════════ | |
| # hermes init — 一键初始化 | |
| # ═══════════════════════════════════════════════════════════════ | |
| def init(force): | |
| """初始化 Hermes:创建配置 → 下载模型 → 验证环境。 | |
| 首次使用只需运行一次。幂等操作,可安全重复执行。""" | |
| config = ensure_config_exists() | |
| click.echo("╔══════════════════════════════════════════╗") | |
| click.echo("║ Hermes 一键初始化向导 ║") | |
| click.echo("╚══════════════════════════════════════════╝\n") | |
| # Step 1: 配置 | |
| click.echo(f"[1/4] 配置文件: {CONFIG_FILE}") | |
| click.echo(f" 数据目录: {config.data_dir}") | |
| click.echo(f" 扫描间隔: {config.scan_interval_seconds}s") | |
| click.echo(f" 匹配阈值: {config.match_threshold}") | |
| # Step 2: 下载模型 | |
| click.echo(f"\n[2/4] Embedding 模型: {config.embedding_model}") | |
| if _check_model_cached() and not force: | |
| click.echo(" ✓ 已缓存") | |
| else: | |
| click.echo(" 正在下载(约 420MB,首次约 2-5 分钟)...") | |
| try: | |
| from hermes_core.embedder import Embedder | |
| e = Embedder(config.embedding_model) | |
| _ = e.encode("test") # 触发加载 | |
| click.echo(" ✓ 下载完成") | |
| except Exception as exc: | |
| click.echo(f" ✗ 下载失败: {exc}") | |
| click.echo(" 可稍后手动运行: hermes download-models") | |
| # Step 3: 检查训练依赖 | |
| click.echo("\n[3/4] QLoRA 训练依赖 (torch + transformers)") | |
| try: | |
| import torch | |
| import transformers | |
| import peft | |
| click.echo(f" ✓ torch {torch.__version__}, transformers {transformers.__version__}") | |
| except ImportError: | |
| click.echo(" ! 未安装(跳过训练的推理模式仍可用)") | |
| click.echo(" 安装训练依赖: pip install -e '.[train]'") | |
| # Step 4: 启动 daemon | |
| click.echo(f"\n[4/4] 下一步") | |
| click.echo(f" hermes start # 后台启动 daemon") | |
| click.echo(f" hermes status # 查看运行状态") | |
| click.echo(f" hermes demo # 运行交互演示") | |
| click.echo(f"\n✅ 初始化完成!") | |
| # ═══════════════════════════════════════════════════════════════ | |
| # hermes download-models — 预下载模型 | |
| # ═══════════════════════════════════════════════════════════════ | |
| def download_models(): | |
| """预下载 embedding 模型(避免首次使用时等待)。""" | |
| config = get_config() | |
| click.echo(f"下载 embedding 模型: {config.embedding_model}") | |
| click.echo("(约 420MB,可能需要 2-5 分钟)\n") | |
| try: | |
| from sentence_transformers import SentenceTransformer | |
| # SentenceTransformer 自带进度条 | |
| model = SentenceTransformer(config.embedding_model) | |
| _ = model.encode("test") | |
| click.echo("\n✅ 模型下载完成") | |
| except Exception as e: | |
| click.echo(f"\n❌ 下载失败: {e}") | |
| click.echo("请检查网络连接,或设置 HF_ENDPOINT=https://hf-mirror.com 使用镜像") | |
| sys.exit(1) | |
| # ═══════════════════════════════════════════════════════════════ | |
| # hermes start / stop / status — daemon 生命周期 | |
| # ═══════════════════════════════════════════════════════════════ | |
| def start(): | |
| """后台启动 Hermes Daemon。""" | |
| config = get_config() | |
| pid_file = Path(config.pid_file) | |
| log_file = Path(config.log_file) | |
| if pid_file.exists(): | |
| pid = int(pid_file.read_text().strip()) | |
| try: | |
| os.kill(pid, 0) | |
| click.echo(f"Daemon 已在运行 (PID: {pid})") | |
| return | |
| except OSError: | |
| pid_file.unlink() | |
| pid_file.parent.mkdir(parents=True, exist_ok=True) | |
| log_file.parent.mkdir(parents=True, exist_ok=True) | |
| log_f = open(str(log_file), "a") | |
| proc = subprocess.Popen( | |
| [sys.executable, "-m", "daemon.daemon"], | |
| stdout=log_f, stderr=log_f, | |
| start_new_session=True, | |
| ) | |
| pid_file.write_text(str(proc.pid)) | |
| time.sleep(1) | |
| if proc.poll() is None: | |
| click.echo(f"✅ Daemon 已启动 (PID: {proc.pid})") | |
| click.echo(f" 日志: {log_file}") | |
| click.echo(f" 查看状态: hermes status") | |
| else: | |
| click.echo(f"❌ Daemon 启动失败,查看日志: {log_file}") | |
| sys.exit(1) | |
| def stop(): | |
| """停止 Hermes Daemon。""" | |
| config = get_config() | |
| pid_file = Path(config.pid_file) | |
| if not pid_file.exists(): | |
| click.echo("Daemon 未在运行") | |
| return | |
| pid = int(pid_file.read_text().strip()) | |
| try: | |
| os.kill(pid, signal.SIGTERM) | |
| for _ in range(10): | |
| try: | |
| os.kill(pid, 0) | |
| time.sleep(0.3) | |
| except OSError: | |
| break | |
| else: | |
| os.kill(pid, signal.SIGKILL) | |
| pid_file.unlink() | |
| click.echo(f"✅ Daemon 已停止 (PID: {pid})") | |
| except OSError: | |
| pid_file.unlink() | |
| click.echo("Daemon 已不在运行") | |
| def status(user): | |
| """查看 Hermes 运行状态。""" | |
| config = get_config() | |
| pid_file = Path(config.pid_file) | |
| output = {"daemon": "stopped", "pid": None} | |
| if pid_file.exists(): | |
| try: | |
| pid = int(pid_file.read_text().strip()) | |
| os.kill(pid, 0) | |
| output["daemon"] = "running" | |
| output["pid"] = pid | |
| except OSError: | |
| pid_file.unlink() | |
| # 汇总统计 | |
| try: | |
| db_path = Path(config.data_dir) / "users" | |
| if db_path.exists(): | |
| users = [d.name for d in db_path.iterdir() if d.is_dir()] | |
| total_records = 0 | |
| total_scopes = 0 | |
| for uid in users: | |
| conn = init_db(uid) | |
| scopes = conn.execute("SELECT COUNT(*) FROM scopes WHERE status='active'").fetchone()[0] | |
| records = conn.execute("SELECT COUNT(*) FROM records WHERE state='active'").fetchone()[0] | |
| conn.close() | |
| total_scopes += scopes | |
| total_records += records | |
| output["users"] = len(users) | |
| output["active_scopes"] = total_scopes | |
| output["active_records"] = total_records | |
| except Exception: | |
| pass | |
| if user: | |
| conn = init_db(user) | |
| rows = conn.execute( | |
| "SELECT id, label, record_count, coherence, status FROM scopes ORDER BY last_activity DESC" | |
| ).fetchall() | |
| conn.close() | |
| output["scopes"] = [dict(r) for r in rows] | |
| _echo_json(output) | |
| # ═══════════════════════════════════════════════════════════════ | |
| # hermes demo — 交互体验 | |
| # ═══════════════════════════════════════════════════════════════ | |
| DEMO_SCENARIOS = [ | |
| { | |
| "desc": "后端API开发", | |
| "dims": [{"key": "language", "value": "TypeScript", "context": "未指定语言时默认"}, | |
| {"key": "framework", "value": "Express", "context": "默认后端框架"}], | |
| }, | |
| { | |
| "desc": "后端API开发", | |
| "dims": [{"key": "language", "value": "TypeScript", "context": "未指定语言时默认"}, | |
| {"key": "framework", "value": "Express", "context": "默认后端框架"}, | |
| {"key": "database", "value": "PostgreSQL", "context": "默认数据库"}], | |
| }, | |
| { | |
| "desc": "前端开发", | |
| "dims": [{"key": "framework", "value": "React", "context": "默认前端框架"}, | |
| {"key": "language", "value": "TypeScript", "context": "默认语言"}], | |
| }, | |
| { | |
| "desc": "数据分析", | |
| "dims": [{"key": "language", "value": "Python", "context": "默认"}, | |
| {"key": "lib", "value": "Pandas", "context": "数据处理"}, | |
| {"key": "style", "value": "functional", "context": "函数式风格"}], | |
| }, | |
| { | |
| "desc": "周末活动", | |
| "dims": [{"key": "activity", "value": "户外徒步", "context": "周末偏好"}], | |
| }, | |
| ] | |
| def demo(user): | |
| """运行交互演示:模拟 Agent 记录偏好 → 查询偏好。 | |
| 这是一个沙箱演示,数据写入临时目录,不影响真实数据。""" | |
| import tempfile | |
| config = get_config() | |
| tmp_dir = tempfile.mkdtemp(prefix="hermes_demo_") | |
| # 用临时目录隔离演示数据 | |
| os.environ["HERMES_DATA_DIR"] = str(tmp_dir) | |
| from hermes_core.types import HERMES_DATA_DIR as _H | |
| import hermes_core.db as db_m | |
| import hermes_core.trainer as tr_m | |
| db_m.HERMES_DATA_DIR = Path(tmp_dir) | |
| tr_m.HERMES_DATA_DIR = Path(tmp_dir) | |
| click.echo("╔══════════════════════════════════════════════════════╗") | |
| click.echo("║ Hermes 联想引擎 — 交互演示 ║") | |
| click.echo("╚══════════════════════════════════════════════════════╝\n") | |
| click.echo("模拟场景:一个 AI Agent 在与用户对话中记录偏好,") | |
| click.echo("并在后续对话中自动联想这些偏好。\n") | |
| click.echo(f"数据目录: {tmp_dir}\n") | |
| client = HermesClient(user_id=user, agent_id="demo-agent") | |
| # 阶段 1:记录 | |
| click.echo("━" * 50) | |
| click.echo("阶段 1:Agent 记录用户偏好 (record_detail)") | |
| click.echo("━" * 50) | |
| for i, s in enumerate(DEMO_SCENARIOS, 1): | |
| click.echo(f"\n 对话 #{i}: 用户提到「{s['desc']}」相关需求") | |
| r = client.record(s["desc"], s["dims"]) | |
| dims_str = ", ".join(f"{d['key']}={d['value']}" for d in s["dims"]) | |
| click.echo(f" → Agent 记录: [{dims_str}]") | |
| if r["status"] == "recorded": | |
| click.echo(f" 状态: ✓ 已记录 (scope: {r['scope_id'][:12]}...)") | |
| elif r["status"] == "rejected": | |
| click.echo(f" 状态: ✗ 被拒绝 ({r['reason']})") | |
| else: | |
| click.echo(f" 状态: ↻ {r['status']}") | |
| # 阶段 2:查询 | |
| click.echo("\n" + "━" * 50) | |
| click.echo("阶段 2:Agent 推理前查询偏好 (query)") | |
| click.echo("━" * 50) | |
| queries = [ | |
| "帮我写一个用户登录的REST API", | |
| "做一个数据可视化的Dashboard", | |
| "这周末想出去玩", | |
| ] | |
| for q in queries: | |
| click.echo(f"\n 用户: 「{q}」") | |
| result = client.query(q) | |
| if result.matched_scope: | |
| click.echo(f" → 匹配场景: {result.matched_scope.scope_label} " | |
| f"(置信度 {result.matched_scope.confidence:.0%})") | |
| for pref in result.related_preferences: | |
| click.echo(f" - {pref.key}: {pref.value}") | |
| else: | |
| click.echo(f" → 未匹配到已知场景(将使用默认行为)") | |
| if result.alternative_scopes: | |
| alt = result.alternative_scopes[0] | |
| click.echo(f" 最接近: {alt.scope_label} (置信度 {alt.confidence:.0%})") | |
| # 阶段 3:查看数据 | |
| click.echo("\n" + "━" * 50) | |
| click.echo("阶段 3:查看训练集状态") | |
| click.echo("━" * 50) | |
| conn = init_db(user) | |
| scopes = conn.execute("SELECT id, label, record_count, coherence FROM scopes WHERE status='active'").fetchall() | |
| conn.close() | |
| click.echo(f"\n 共有 {len(scopes)} 个动态场景:") | |
| for s_c in scopes: | |
| click.echo(f" {s_c['id']} — {s_c['label']} " | |
| f"({s_c['record_count']} 条记录, 内聚度 {s_c['coherence']:.2f})") | |
| click.echo(f"\n 📊 数据保存在: {tmp_dir}(可手动删除)") | |
| click.echo(f" 💡 下次演示: hermes demo --user {user}") | |
| import shutil | |
| shutil.rmtree(tmp_dir, ignore_errors=True) | |
| # ═══════════════════════════════════════════════════════════════ | |
| # hermes record — 写入训练样本 | |
| # ═══════════════════════════════════════════════════════════════ | |
| def record(user, scope_desc, dimensions, source_conv, source_agent): | |
| """写入一条训练样本。""" | |
| try: | |
| dims = json.loads(dimensions) | |
| except json.JSONDecodeError: | |
| _echo_json({"status": "error", "reason": "Invalid dimensions JSON"}) | |
| sys.exit(1) | |
| client = HermesClient(user_id=user, agent_id=source_agent) | |
| result = client.record(scope_desc, dims, source_conv=source_conv) | |
| _echo_json(result) | |
| # ═══════════════════════════════════════════════════════════════ | |
| # hermes refine — 调整场景粒度 | |
| # ═══════════════════════════════════════════════════════════════ | |
| def refine(user, record_id, scope_desc, direction): | |
| """调整记录的场景粒度。""" | |
| from hermes_core.embedder import Embedder | |
| embedder = Embedder() | |
| result = refine_scene(user, record_id, scope_desc, direction, embedder) | |
| _echo_json(result) | |
| # ═══════════════════════════════════════════════════════════════ | |
| # hermes query — 场景识别 + 偏好查询 | |
| # ═══════════════════════════════════════════════════════════════ | |
| def query(user, text): | |
| """场景识别 + 偏好查询。""" | |
| client = HermesClient(user_id=user, agent_id="cli") | |
| result = client.query(text) | |
| output = { | |
| "matched_scope": None, | |
| "active_loras": [], | |
| "related_preferences": [], | |
| "training_outdated": result.training_outdated, | |
| } | |
| if result.matched_scope: | |
| output["matched_scope"] = { | |
| "scope_id": result.matched_scope.scope_id, | |
| "scope_label": result.matched_scope.scope_label, | |
| "confidence": round(result.matched_scope.confidence, 4), | |
| } | |
| if result.alternative_scopes: | |
| output["alternative_scopes"] = [ | |
| {"scope_id": s.scope_id, "scope_label": s.scope_label, | |
| "confidence": round(s.confidence, 4)} | |
| for s in result.alternative_scopes[:3] | |
| ] | |
| output["active_loras"] = [ | |
| {"scope_id": l.scope_id, "version": l.version, "priority": l.priority} | |
| for l in result.active_loras | |
| ] | |
| output["related_preferences"] = [ | |
| {"key": p.key, "value": p.value, "source": p.source} | |
| for p in result.related_preferences | |
| ] | |
| _echo_json(output) | |
| # ═══════════════════════════════════════════════════════════════ | |
| # hermes review — 训练集审核 | |
| # ═══════════════════════════════════════════════════════════════ | |
| def review(): | |
| """训练集审核。""" | |
| def review_pending(user, scope_id): | |
| """列出待审核记录。""" | |
| conn = init_db(user) | |
| if scope_id: | |
| records = get_active_records(conn, scope_id) | |
| else: | |
| from hermes_core.db import get_active_scopes | |
| scopes = get_active_scopes(conn) | |
| records = [] | |
| for s in scopes: | |
| records.extend(get_active_records(conn, s.id)) | |
| conn.close() | |
| click.echo(json.dumps([ | |
| { | |
| "id": r.id, "scope_id": r.scope_id, "scope_label": r.scope_label, | |
| "dimensions": [{"key": d.key, "value": d.value} for d in r.dimensions], | |
| "confidence": r.confidence, "occurrences": r.occurrences, | |
| } | |
| for r in records | |
| ], ensure_ascii=False, indent=2)) | |
| def review_accept(user, record_ids): | |
| """批量通过记录。""" | |
| conn = init_db(user) | |
| for rid in record_ids: | |
| update_record_state(conn, rid, RecordState.active) | |
| conn.close() | |
| _echo_json({"status": "accepted", "count": len(record_ids)}) | |
| def review_reject(user, reason, record_ids): | |
| """批量拒绝记录。""" | |
| conn = init_db(user) | |
| for rid in record_ids: | |
| update_record_state(conn, rid, RecordState.rejected) | |
| conn.close() | |
| _echo_json({"status": "rejected", "count": len(record_ids), "reason": reason}) | |
| # ═══════════════════════════════════════════════════════════════ | |
| # hermes train-status / train — 训练管理 | |
| # ═══════════════════════════════════════════════════════════════ | |
| def train_status(user, scope_id): | |
| """查询训练任务状态。""" | |
| conn = init_db(user) | |
| if scope_id: | |
| rows = conn.execute( | |
| "SELECT * FROM training_runs WHERE scope_id=? ORDER BY version DESC LIMIT 3", | |
| (scope_id,) | |
| ).fetchall() | |
| else: | |
| rows = conn.execute( | |
| "SELECT * FROM training_runs ORDER BY started_at DESC LIMIT 10" | |
| ).fetchall() | |
| conn.close() | |
| click.echo(json.dumps([ | |
| {"id": r["id"], "scope_id": r["scope_id"], "version": r["version"], | |
| "status": r["status"], "started_at": r["started_at"] or "N/A", | |
| "finished_at": r["finished_at"] or "N/A"} | |
| for r in rows | |
| ], ensure_ascii=False, indent=2)) | |
| def train(user, scope_id): | |
| """手动触发训练。""" | |
| _echo_json({ | |
| "status": "queued", | |
| "message": f"Training for {scope_id} queued. Use 'train-status' to check progress." | |
| }) | |
| # ═══════════════════════════════════════════════════════════════ | |
| # hermes config — 查看/修改配置 | |
| # ═══════════════════════════════════════════════════════════════ | |
| def config_cmd(): | |
| """查看和修改配置。""" | |
| pass | |
| def config_show(): | |
| """显示当前配置。""" | |
| config = get_config() | |
| _echo_json(config.to_dict()) | |
| def config_set(key, value): | |
| """修改配置项。例如: hermes config set match_threshold 0.65""" | |
| config = get_config() | |
| if key not in HermesConfig.__dataclass_fields__: | |
| click.echo(f"未知配置项: {key}") | |
| click.echo(f"可用配置项: {', '.join(HermesConfig.__dataclass_fields__.keys())}") | |
| sys.exit(1) | |
| field_type = type(getattr(config, key)) | |
| try: | |
| setattr(config, key, field_type(value)) | |
| except (ValueError, TypeError) as e: | |
| click.echo(f"值类型错误: {e}") | |
| sys.exit(1) | |
| save_config(config) | |
| click.echo(f"✅ {key} = {getattr(config, key)}") | |
| # ═══════════════════════════════════════════════════════════════ | |
| # hermes daemon — 启动/停止(兼容旧接口) | |
| # ═══════════════════════════════════════════════════════════════ | |
| def daemon(foreground): | |
| """启动 Daemon(前台),建议用 hermes start 后台运行。""" | |
| if foreground: | |
| from daemon.daemon import main as daemon_main | |
| click.echo("Hermes Daemon 前台运行中 (Ctrl+C 停止)") | |
| daemon_main() | |
| else: | |
| click.echo("请用 hermes start 后台启动,或 hermes daemon --foreground 前台运行") | |
| if __name__ == "__main__": | |
| cli() | |