diff --git a/hosts/mars/hermes-agent.nix b/hosts/mars/hermes-agent.nix index 417c32d..0d84e48 100644 --- a/hosts/mars/hermes-agent.nix +++ b/hosts/mars/hermes-agent.nix @@ -74,6 +74,9 @@ let builtins.readFile ./gitea-pr-review-prompt.md ); + # Mnemosyne memory provider: third-party plugin, not in the image. + mnemosyneEnv = pkgs.callPackage ../../pkgs/mnemosyne-env.nix { }; + # 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 +421,33 @@ in mv -f "$tmp" "$conf" ''; }; + + # Hermes discovers memory providers under $HERMES_HOME/plugins; the target + # is a store path, readable in the container via the /nix/store ro mount. + # wantedBy, not requiredBy: a failure leaves Hermes on built-in memory. + systemd.services.hermes-agent-mnemosyne-plugin = { + description = "Link Mnemosyne provider into the Hermes plugin dir"; + before = [ "podman-hermes-agent.service" ]; + wantedBy = [ "podman-hermes-agent.service" ]; + after = [ "hermes-agent-prepare-dirs.service" ]; + requires = [ "hermes-agent-prepare-dirs.service" ]; + path = [ pkgs.coreutils ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + set -euo pipefail + pluginsDir=${hermesHome}/plugins + pluginDir=$pluginsDir/mnemosyne + target=${mnemosyneEnv}/${mnemosyneEnv.sitePackages}/hermes_memory_provider + [ -d "$target" ] || { echo "$target missing" >&2; exit 1; } + + mkdir -p "$pluginsDir" + chown ${hermesUid}:${hermesGid} "$pluginsDir" + ln -sfn "$target" "$pluginDir.new" + mv -Tf "$pluginDir.new" "$pluginDir" + chown -h ${hermesUid}:${hermesGid} "$pluginDir" + ''; + }; } diff --git a/pkgs/mnemosyne-env.nix b/pkgs/mnemosyne-env.nix new file mode 100644 index 0000000..bc57508 --- /dev/null +++ b/pkgs/mnemosyne-env.nix @@ -0,0 +1,56 @@ +# Mnemosyne memory provider for Hermes. Built here rather than pip-installed: +# the image's Python has no pip and is PEP 668 managed. +# +# Core deps only: the embeddings extra (fastembed/onnxruntime) is optional at +# runtime, and recall falls back to FTS5. +{ + python3, + fetchPypi, +}: + +let + mnemosyneMemory = python3.pkgs.buildPythonPackage rec { + pname = "mnemosyne-memory"; + version = "3.15.1"; + pyproject = true; + + src = fetchPypi { + pname = "mnemosyne_memory"; + inherit version; + hash = "sha256-lspUMxc0pUSkhSUrNdiiO5OJ1NMC/S853EYSanXtXKM="; + }; + + build-system = with python3.pkgs; [ setuptools ]; + dependencies = with python3.pkgs; [ pyyaml ]; + + doCheck = false; # tests want a live Hermes + LLM key + # Ships the Hermes plugin package too, not just the core. + pythonImportsCheck = [ + "mnemosyne" + "hermes_memory_provider" + ]; + }; + + mnemosyneHermes = python3.pkgs.buildPythonPackage rec { + pname = "mnemosyne-hermes"; + version = "0.5.0"; + pyproject = true; + + src = fetchPypi { + pname = "mnemosyne_hermes"; + inherit version; + hash = "sha256-CzEvnUw5oPFtT5bHQQ/GBdy2C/E7qShQn32irIRYKqw="; + }; + + build-system = with python3.pkgs; [ setuptools ]; + # Upstream asks for mnemosyne-memory[embeddings]; see the header. + dependencies = [ mnemosyneMemory ] ++ (with python3.pkgs; [ pyyaml ]); + + doCheck = false; + pythonImportsCheck = [ "mnemosyne_hermes" ]; + }; +in +python3.withPackages (_: [ + mnemosyneMemory + mnemosyneHermes +])