Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/hermes-catalog-prep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"upstash-hermes-memory": patch
---

Prepare for the Hermes plugin catalog: add `plugin.yaml` and a directory `__init__.py`, cap
`upstash-redis` below 2, and document telemetry and how to turn it off.
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1424,7 +1424,8 @@ Python lives **beside** the pnpm workspace, not instead of any of it:
and writes its `CHANGELOG.md`. `privatePackages: { version: true, tag: false }`: changesets never
tags or publishes it.
- `scripts/sync-version.mjs` copies that version into `pyproject.toml` and
`src/<module>/_version.py` (`VERSION`, for telemetry), discovering packages by folder; `ci:version`
`src/<module>/_version.py` (`VERSION`, for telemetry), plus `plugin.yaml` when the package has one (the
Hermes plugin manifest), discovering packages by folder; `ci:version`
then runs `uv lock`, so the Version Packages PR carries a consistent `uv.lock`. **Never edit a
Python version by hand** — `sync-version.mjs --check` fails CI on drift.
- `scripts/publish-python.mjs` (`pnpm ci:publish:python`) is PyPI's `changeset publish`: for every
Expand Down
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,13 @@ dev = ["pytest>=8", "ruff>=0.6"]
line-length = 100
target-version = "py311"

# A Hermes directory plugin's entry point is `__init__.py` at the plugin root, and the root is a
# hyphenated `python/<name>` folder.
[tool.ruff.lint.per-file-ignores]
"python/*/__init__.py" = ["N999"]

[tool.pytest.ini_options]
testpaths = ["python"]
# The plugin-root `__init__.py` makes `python/<name>` look like a package to the default
# `prepend` mode, which then fails to import it; importlib mode imports test files on their own.
addopts = ["--import-mode=importlib"]
7 changes: 7 additions & 0 deletions python/hermes-memory/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ these):
| `UPSTASH_HERMES_USER_ID` | Whose memories these are. Defaults to the gateway user, then the Hermes profile name, then `default`. |
| `UPSTASH_HERMES_MEMORY_PREFIX`, `UPSTASH_HERMES_CHAT_PREFIX` | Key prefixes. Default `agentkit:memory` and `agentkit:chat`, the TS SDK's defaults, so the two share memories. |

## Telemetry

On by default, like the other Upstash SDKs: requests to your Upstash database carry
`upstash-hermes-memory@<version>` in the `Upstash-Telemetry-Sdk` header, next to the tag
`upstash-redis` already sends. Nothing else is collected and nothing goes anywhere but your
database. Set `UPSTASH_DISABLE_TELEMETRY=1` to turn it off.

## Redis layout

| | Key | Index |
Expand Down
10 changes: 10 additions & 0 deletions python/hermes-memory/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Entry point for directory installs (the Hermes plugin catalog).

The catalog clones this directory and imports its `__init__.py`, so it re-exports `register` from
the package under `src/`. pip installs skip this file and use the `hermes_agent.memory_providers`
entry point in pyproject.toml.
"""

from .src.upstash_hermes_memory import register

__all__ = ["register"]
Comment on lines +1 to +10
12 changes: 12 additions & 0 deletions python/hermes-memory/plugin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Manifest Hermes reads when this directory is installed as a plugin (the plugin catalog points
# `subdir` here). pip installs use the entry point in pyproject.toml instead. Catalog rules:
# https://hermes-agent.nousresearch.com/docs/developer-guide/plugins/catalog-submission
name: upstash
# Copied from package.json by scripts/sync-version.mjs, like pyproject.toml.
version: "0.1.0"
description: "Upstash Redis memory provider: long-term memory, searchable chat history and recall on Redis Search, sharing keys with @upstash/agentkit-sdk."
# Also declared in the catalog entry; older Hermes releases only enforce this one (catalog rule 14).
requires_hermes: ">=0.19"
requires_env:
- UPSTASH_REDIS_REST_URL
- UPSTASH_REDIS_REST_TOKEN
3 changes: 2 additions & 1 deletion python/hermes-memory/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ description = "Upstash Redis memory provider for Hermes Agent: long-term memory,
readme = "README.md"
license = "MIT"
requires-python = ">=3.11"
dependencies = ["upstash-redis>=1.8.0"]
# Upper bound: the Hermes plugin catalog asks for one on every dependency (catalog rule 10).
dependencies = ["upstash-redis>=1.8.0,<2"]

# Hermes discovers pip-installed memory providers through this group; the entry-point name is the
# value of `memory.provider` in Hermes's config.yaml.
Expand Down
6 changes: 6 additions & 0 deletions python/hermes-memory/tests/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ def test_version_matches_package_json():
assert VERSION == package_json["version"]


def test_version_matches_plugin_manifest():
# Catalog installs read plugin.yaml, not pyproject.toml; sync-version.mjs stamps both.
manifest = (PACKAGE_DIR / "plugin.yaml").read_text()
assert re.search(r'^version: "(.*)"$', manifest, re.MULTILINE).group(1) == VERSION


def test_package_json_names_the_distribution():
# sync-version.mjs derives the module path from this name, and publish-python.mjs queries PyPI
# with it, so it has to be the PyPI project name exactly.
Expand Down
10 changes: 10 additions & 0 deletions scripts/sync-version.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ for (const dir of pythonDirs) {
version,
pyproject.replace(/^version = ".*"$/m, `version = "${version}"`),
);
// A Hermes plugin manifest (catalog installs read it instead of pyproject.toml) carries the version too.
if (existsSync(join(root, pkgDir, "plugin.yaml"))) {
const manifest = readFileSync(join(root, pkgDir, "plugin.yaml"), "utf8");
stamp(
pkgDir,
"plugin.yaml",
version,
manifest.replace(/^version: .*$/m, `version: "${version}"`),
);
}
const { name } = JSON.parse(readFileSync(join(root, pkgDir, "package.json"), "utf8"));
stamp(
pkgDir,
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading