mars: provision the Mnemosyne memory provider for Hermes #6
@@ -74,6 +74,17 @@ let
|
||||
builtins.readFile ./gitea-pr-review-prompt.md
|
||||
);
|
||||
|
||||
# Mnemosyne memory provider (local SQLite, third-party plugin — not bundled
|
||||
# with Hermes). Requirements pins live in ./mnemosyne/requirements.txt; see
|
||||
# the provisioning unit near the bottom of this file for the layout mapped
|
||||
# into hermesHome. The symlink target below must be the SAME path the
|
||||
# side venv was built with (hermesHome/mnemosyne-venv) — Hermes resolves
|
||||
# plugin modules through it, so relative traversal after the bind mount
|
||||
# still resolves inside the container identically.
|
||||
mnemosyneReqs = pkgs.writeText "mnemosyne-requirements.txt" (
|
||||
builtins.readFile ./mnemosyne/requirements.txt
|
||||
);
|
||||
|
||||
# Wire event names (X-GitHub-Event) each route accepts — NOT the
|
||||
# subscription names the gitea hooks in services/dev/gitea.nix use. The two
|
||||
# namespaces collide; see the long comment on the route unit below.
|
||||
@@ -418,4 +429,116 @@ in
|
||||
mv -f "$tmp" "$conf"
|
||||
'';
|
||||
};
|
||||
|
||||
# ---- Mnemosyne memory provider ----------------------------------------
|
||||
# Third-party plugin (PyPI: mnemosyne-hermes + mnemosyne-memory), not
|
||||
# bundled with the official image. Two pieces must exist before the gateway
|
||||
# starts for memory.provider = mnemosyne to activate:
|
||||
#
|
||||
# 1. ${hermesHome}/plugins/mnemosyne — a symlink to the plugin package
|
||||
# inside the side venv. Hermes discovers providers by scanning
|
||||
# $HERMES_HOME/plugins (see its plugins/memory discovery code), reads
|
||||
# __init__.py/. For it to IMPORT cleanly the plugin's sibling
|
||||
# `mnemosyne` core package must be importable too — which is exactly
|
||||
# why the plugin code lives inside the side venv's site-packages
|
||||
# rather than as a bare writable copy.
|
||||
#
|
||||
# 2. The side venv itself (${hermesHome}/mnemosyne-venv), built with the
|
||||
# pinned pins in ./mnemosyne/requirements.txt. Inside hermesHome so
|
||||
# it lands inside HERMES_WRITE_SAFE_ROOT=/opt/data (visible to the
|
||||
# container at /opt/data/mnemosyne-venv) and survives image rebuilds.
|
||||
#
|
||||
# The venv's absolute paths embed ${hermesHome}: uv venv records the
|
||||
# creation prefix, which is by construction identical inside and outside
|
||||
# the container thanks to /opt/data being a bind mount of hermesHome.
|
||||
#
|
||||
# Idempotent: marked done by a stamp file keyed by the hash of the
|
||||
# requirements text, so a changed pin re-provisions. Never deletes —
|
||||
# removing memory.provider from config is what retires it.
|
||||
#
|
||||
# Ordering: before podman-hermes-agent (the gateway needs the plugin at
|
||||
# import time), after network (uv may fetch wheels on first provision),
|
||||
# with a bounded timeout so a broken proxy cannot hang boot.
|
||||
systemd.services.hermes-agent-mnemosyne-provision = {
|
||||
description = "Provision Mnemosyne memory provider (side venv + plugin symlink)";
|
||||
before = [ "podman-hermes-agent.service" ];
|
||||
wantedBy = [ "podman-hermes-agent.service" ];
|
||||
wants = [ "network-online.target" ];
|
||||
after = [ "network-online.target" ];
|
||||
unitConfig.RequiresMountsFor = [ "/mnt/jupiter" ];
|
||||
path = [ pkgs.python3 pkgs.uv pkgs.coreutils ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
TimeoutStartSec = 600;
|
||||
};
|
||||
environment = {
|
||||
UV_PYTHON_INSTALL_DIR = "${hermesHome}/mnemosyne-uv/python";
|
||||
UV_CACHE_DIR = "${hermesHome}/mnemosyne-uv/cache";
|
||||
UV_COMPILE_BYTECODE = "1";
|
||||
};
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
venv=${hermesHome}/mnemosyne-venv
|
||||
pluginDir=${hermesHome}/plugins/mnemosyne
|
||||
stampFile=${hermesHome}/mnemosyne-provision.stamp
|
||||
reqHash=$(sha256sum ${mnemosyneReqs} | cut -d" " -f1)
|
||||
|
||||
if [ -x "$venv/bin/python" ] && [ -f "$stampFile" ] \
|
||||
&& [ "$(cat "$stampFile")" = "$reqHash" ] \
|
||||
&& [ -e "$pluginDir" ] \
|
||||
&& [ -x "$venv/bin/mnemosyne-hermes" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p ${hermesHome}/plugins ${hermesHome}/mnemosyne
|
||||
uv venv "$venv" --python ${pkgs.python313}/bin/python3 --quiet
|
||||
UV_VENV="$venv" uv pip install \
|
||||
--python "$venv/bin/python" \
|
||||
--requirement ${mnemosyneReqs} --quiet
|
||||
|
||||
# The plugin wrapper lands as hermes_memory_provider inside site-packages;
|
||||
# uv installs the exact entry point scripts shown below. Symlink the
|
||||
# discovered package dir (never a fixed guess — find it by marker).
|
||||
siteDir=$("$venv/bin/python" -c 'import site; print(site.getsitepackages()[0])')
|
||||
target="$siteDir/hermes_memory_provider"
|
||||
[ -d "$target" ] || { echo "mnemosyne plugin package not found in venv" >&2; exit 1; }
|
||||
|
||||
install -d -m 0755 -o ${hermesUid} -g ${hermesGid} \
|
||||
"$(dirname "$pluginDir")"
|
||||
rm -f "$pluginDir"
|
||||
ln -s "$target" "$pluginDir"
|
||||
|
||||
printf '%s' "$reqHash" > "$stampFile"
|
||||
chown -R ${hermesUid}:${hermesGid} \
|
||||
"$venv" "$(dirname "$pluginDir")" "$stampFile" \
|
||||
${hermesHome}/mnemosyne-uv
|
||||
|
||||
# Mirror the "active provider" cue into config.yaml — equivalent to
|
||||
# `hermes config set memory.provider mnemosyne`, but idempotent and
|
||||
# non-interactive. Only touches the one key, never rewrites the file.
|
||||
cfg=${hermesHome}/config.yaml
|
||||
if [ -f "$cfg" ]; then
|
||||
if ! grep -q '^ provider: mnemosyne' "$cfg"; then
|
||||
if grep -q '^memory:' "$cfg"; then
|
||||
sed -i 's/^memory:$/memory:\n provider: mnemosyne/' "$cfg"
|
||||
else
|
||||
printf '\nmemory:\n provider: mnemosyne\n' >> "$cfg"
|
||||
fi
|
||||
chown ${hermesUid}:${hermesGid} "$cfg"
|
||||
fi
|
||||
else
|
||||
printf 'memory:\n provider: mnemosyne\n' > "$cfg"
|
||||
chown ${hermesUid}:${hermesGid} "$cfg"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
# Also assert mnemosyne as the active provider so the container's own
|
||||
# config.yaml says the same thing statelessly — mirrored from the docs'
|
||||
# `hermes config set memory.provider mnemosyne`. Done here (not a separate
|
||||
# unit) so venv and config-cue stay in lockstep; never edits anything else
|
||||
# in the file. Runs at the tail of the provisioning oneshot, after a
|
||||
# successful venv, so a half-provision never flips the provider on.
|
||||
# (system.activationScripts is NOT used — the file must exist first, and
|
||||
# activation would run before hermesHome's own cont-init has created it.)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Pinned requirements for a Mnemosyne side-venv on mars.
|
||||
#
|
||||
# Hermes vendors its own Python (the official image's venv) and deliberately
|
||||
# stays minimal: no pip module inside it, PEP 668 external-management on top.
|
||||
# Installing provider packages straight into that interpreter would fight the
|
||||
# image on every rebuild, so Mnemosyne (and its plugin wrapper) live in their
|
||||
# own venv instead — see the provisioning unit in hosts/mars/hermes-agent.nix.
|
||||
#
|
||||
# Freeze captured 2026-09-19 from a verified container-side install of
|
||||
# `mnemosyne-memory[embeddings]` + `mnemosyne-hermes` — side venv at
|
||||
# $HERMES_HOME/mnemosyne-venv, activated via $HERMES_HOME/plugins/mnemosyne.
|
||||
# Versions pinned exactly; transitive deps frozen for reproducibility
|
||||
# (onnxruntime/numpy drift under a long-lived SQLite state dir is what a
|
||||
# freeze is here to prevent).
|
||||
#
|
||||
anyio==4.15.0
|
||||
certifi==2026.7.22
|
||||
charset-normalizer==3.5.1
|
||||
click==8.5.0
|
||||
fastembed==0.8.0
|
||||
filelock==3.32.5
|
||||
flatbuffers==25.12.19
|
||||
fsspec==2026.7.0
|
||||
h11==0.16.0
|
||||
hf-xet==1.6.0
|
||||
httpcore==1.0.9
|
||||
httpx==0.28.1
|
||||
huggingface-hub==1.32.0
|
||||
idna==3.19
|
||||
loguru==0.7.3
|
||||
mmh3==5.3.0
|
||||
mnemosyne-hermes==0.5.0
|
||||
mnemosyne-memory==3.15.1
|
||||
numpy==2.5.3
|
||||
onnxruntime==1.30.0
|
||||
packaging==26.3
|
||||
pillow==12.3.0
|
||||
protobuf==7.36.1
|
||||
py-rust-stemmers==0.1.8
|
||||
pyyaml==6.0.3
|
||||
requests==2.34.2
|
||||
sqlite-vec==0.1.9
|
||||
tokenizers==0.23.2
|
||||
tqdm==4.70.0
|
||||
typing-extensions==4.16.0
|
||||
urllib3==2.7.0
|
||||
Reference in New Issue
Block a user