mars: provision the Mnemosyne memory provider for Hermes #6
@@ -74,6 +74,17 @@ let
|
|||||||
builtins.readFile ./gitea-pr-review-prompt.md
|
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
|
# Wire event names (X-GitHub-Event) each route accepts — NOT the
|
||||||
# subscription names the gitea hooks in services/dev/gitea.nix use. The two
|
# subscription names the gitea hooks in services/dev/gitea.nix use. The two
|
||||||
# namespaces collide; see the long comment on the route unit below.
|
# namespaces collide; see the long comment on the route unit below.
|
||||||
@@ -418,4 +429,116 @@ in
|
|||||||
mv -f "$tmp" "$conf"
|
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