Skip to content

Commit 842b205

Browse files
authored
Add entities namespace and activate memory pipeline hooks (#4)
1 parent 42156bf commit 842b205

38 files changed

Lines changed: 4120 additions & 79 deletions

.github/workflows/publish-pypi.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Publish atomicmemory to PyPI via OIDC Trusted Publishing.
2+
#
3+
# Runs ONLY on the public mirror (atomicstrata/atomicmemory-python), which is
4+
# the clean release surface under the manual-snapshot sync model: this file
5+
# rides a snapshot PR out from am-python-internal, but the `if` guard keeps it
6+
# inert on the private dev repo (which has no PyPI trusted-publisher binding).
7+
#
8+
# Manual, version-gated dispatch — never on push/tag — so a routine snapshot
9+
# sync can never trigger a publish. The `pypi-release` environment supplies the
10+
# human-approval gate that the local `uv publish` workflow used to provide.
11+
name: Publish to PyPI
12+
13+
on:
14+
workflow_dispatch:
15+
inputs:
16+
version:
17+
description: "Version to publish — must equal pyproject.toml's version and not already exist on PyPI."
18+
required: true
19+
type: string
20+
21+
permissions:
22+
contents: read
23+
24+
jobs:
25+
publish:
26+
if: github.repository == 'atomicstrata/atomicmemory-python'
27+
runs-on: ubuntu-latest
28+
environment: pypi-release
29+
permissions:
30+
id-token: write # OIDC token for Trusted Publishing
31+
steps:
32+
- uses: actions/checkout@v4
33+
34+
- name: Install uv
35+
uses: astral-sh/setup-uv@v4
36+
37+
- name: Verify version matches and is unpublished
38+
run: |
39+
pkg_version="$(sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml | head -1)"
40+
if [ "${pkg_version}" != "${{ inputs.version }}" ]; then
41+
echo "::error::pyproject.toml version '${pkg_version}' != requested '${{ inputs.version }}'"
42+
exit 1
43+
fi
44+
status="$(curl -s -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/atomicmemory/${pkg_version}/json")"
45+
if [ "${status}" = "200" ]; then
46+
echo "::error::atomicmemory ${pkg_version} is already on PyPI; bump the version first"
47+
exit 1
48+
fi
49+
echo "Publishing atomicmemory ${pkg_version} (PyPI returned ${status} for the existence check)."
50+
51+
- name: Build sdist + wheel
52+
run: uv build
53+
54+
- name: Publish to PyPI (Trusted Publishing, with attestations)
55+
uses: pypa/gh-action-pypi-publish@release/v1

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Before changing code, read the relevant local files first:
4949
Run before opening any PR:
5050

5151
```bash
52-
uv sync
52+
uv sync --all-extras # installs dev + embeddings extras; strict mypy needs them
5353
uv run ruff check .
5454
uv run ruff format --check .
5555
uv run mypy atomicmemory --strict

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@ All notable changes to `atomicmemory` will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7-
## [Unreleased]
7+
## [1.1.1] - 2026-06-11
8+
9+
### Added
10+
- `UnsupportedOperationError` (subclass of `ProviderError`) and `InvalidScopeError` (subclass of `ValidationError`), raised where those parents were previously raised bare: a provider missing the `package` extension, and an operation missing required scope fields. Existing `except ProviderError` / `except ValidationError` handlers keep working; consumers can now catch the specific types, matching the TS SDK. Exported from the package root.
11+
- `AsyncMemoryProcessingPipeline` + `NOOP_ASYNC_PIPELINE` (exported from `atomicmemory.memory` alongside the existing sync type): the async-surface pipeline type used by `AsyncProviderRegistration`.
12+
- `EntitiesClient` / `AsyncEntitiesClient`: the `entities` namespace over `/v1/entities`, ported from the TS SDK — entity profiles, listing, detail, cascade delete, attribute triples, per-memory history, per-entity settings patching, and entity merge. Wired into `AtomicMemoryClient`/`AsyncAtomicMemoryClient` as `.entities` on the same transport config, closed with the client. Python field names match the snake_case wire directly (the TS camelCase mapping layer has no Python counterpart).
13+
14+
### Changed
15+
- Registered memory pipelines now actually execute: `MemoryService`/`AsyncMemoryService` run `preprocess_ingest` (which may split one input into many; per-item results merge in order), `postprocess_ingest`, `preprocess_search`/`postprocess_search` (postprocess receives the processed request), `preprocess_get`/`postprocess_get`, and `postprocess_list`, matching the TS `MemoryService` semantics. `delete` and `package` take no pipeline. Before 1.1.1 these hooks were accepted at registration but never invoked. Hook exceptions propagate unwrapped — hooks are caller-supplied code.
16+
- `MemoryProcessingPipeline` hook signatures are now synchronous (the async surface uses `AsyncMemoryProcessingPipeline`). Type-hint change only for code that constructed pipelines — which received no behavior before this release.
817

918
## [1.1.0] - 2026-06-09
1019

README.md

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ This is a Python port of the TypeScript [`atomicmemory-sdk`](https://github.com/
2121

2222
## Status
2323

24-
Stable release — `1.0.0` on [PyPI](https://pypi.org/project/atomicmemory/).
24+
Stable release — `1.1.0` on [PyPI](https://pypi.org/project/atomicmemory/); `1.2.0` staged on main.
2525

2626
## Installation
2727

@@ -131,6 +131,64 @@ The `client.storage` namespace mirrors the TypeScript SDK's direct storage API:
131131

132132
Every storage request sends `Authorization: Bearer <apiKey>` and `X-AtomicMemory-User-Id`. The SDK never sends the legacy `?user_id=` URL parameter.
133133

134+
135+
## Entities
136+
137+
The `client.entities` namespace (on `AtomicMemoryClient` and `AsyncAtomicMemoryClient`) provides typed access to the `/v1/entities` API — profiles, attributes, memory history, settings, and entity merge.
138+
139+
```python
140+
from atomicmemory import AtomicMemoryClient
141+
142+
with AtomicMemoryClient({
143+
"apiUrl": "http://localhost:17350",
144+
"apiKey": "server-api-key",
145+
"userId": "demo",
146+
}) as client:
147+
# fetch the synthesized profile for a user
148+
profile = client.entities.profile("alice")
149+
print(profile.entity_id, profile.summary)
150+
151+
# list all entities (paginated)
152+
result = client.entities.list(page=1, page_size=20)
153+
for entity in result.entities:
154+
print(entity.entity_id, entity.memory_count)
155+
```
156+
157+
The async surface is identical — call `await client.entities.profile("alice")` on `AsyncAtomicMemoryClient`.
158+
159+
## Memory pipelines
160+
161+
`MemoryProcessingPipeline` (and its async twin `AsyncMemoryProcessingPipeline`) let you attach optional pre- and post-processing hooks to any registered provider. All hook fields are `None` by default, so a pipeline with only one hook populated is valid.
162+
163+
```python
164+
from atomicmemory import AtomicMemoryClient
165+
from atomicmemory.memory.pipeline import MemoryProcessingPipeline
166+
from atomicmemory.memory.registry import ProviderRegistration, default_registry
167+
168+
def split_long_content(input):
169+
# return a list of IngestInput items; here we pass through unchanged
170+
return [input]
171+
172+
def log_ingest_result(result, original_input):
173+
print(f"ingested: {len(result.created)} created, {len(result.updated)} updated")
174+
175+
pipeline = MemoryProcessingPipeline(
176+
preprocess_ingest=split_long_content, # optional — splits one input into many
177+
postprocess_ingest=log_ingest_result, # optional — runs after each per-item ingest
178+
)
179+
180+
# Register the pipeline alongside a provider factory
181+
def my_provider_factory(config):
182+
from atomicmemory.memory.provider import BaseMemoryProvider
183+
# ... build and return your provider ...
184+
provider = ...
185+
return ProviderRegistration(provider=provider, pipeline=pipeline)
186+
187+
default_registry.register("my_provider", my_provider_factory)
188+
```
189+
190+
If `preprocess_ingest` splits one input into N items and a per-item ingest raises mid-loop, earlier items are already persisted and no merged result is returned — keep splitting pipelines idempotent.
191+
134192
## v1 wire contract
135193

136194
`atomicmemory.contract.v1` is the wire codec for the v1 provider-contract encoding. The wire form is deliberately mixed-case — `Memory.createdAt`/`updatedAt` and `SearchResult.rankingScore` are camelCase; `version_id`, `observed_at`, and retrieval-receipt fields are snake_case — as pinned by the vendored `contract/CONTRACT.md`. This module is the only place that mapping lives; in-process models and provider mappers are unchanged.

RELEASING.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Releasing `atomicmemory` to PyPI
2+
3+
Publishing uses **OIDC Trusted Publishing** — no API tokens. The package is
4+
published from the public mirror (`atomicstrata/atomicmemory-python`), which is
5+
the clean release surface; development happens here in the private repo and
6+
reaches the mirror via a manual snapshot sync.
7+
8+
## One-time setup (already done unless this is a fresh project)
9+
10+
1. **PyPI Trusted Publisher** — on the `atomicmemory` project at
11+
<https://pypi.org/manage/project/atomicmemory/settings/publishing/>, add a
12+
GitHub publisher:
13+
- Owner: `atomicstrata`
14+
- Repository: `atomicmemory-python`
15+
- Workflow filename: `publish-pypi.yml`
16+
- Environment name: `pypi-release`
17+
2. **GitHub environment** — on `atomicstrata/atomicmemory-python`, create an
18+
environment named `pypi-release`. Add the release approver as a required
19+
reviewer to gate each publish behind a one-click approval.
20+
21+
## Releasing a version
22+
23+
1. Land the version bump + changelog on this repo's `main` (the `[Unreleased]`
24+
CHANGELOG section becomes `[X.Y.Z] - <date>` at release time).
25+
2. Snapshot-sync `main` to `atomicstrata/atomicmemory-python` (single curated PR;
26+
merge after its CI is green).
27+
3. On the mirror, run the **Publish to PyPI** workflow
28+
(Actions → Publish to PyPI → Run workflow) with the exact version as input.
29+
It verifies the version matches `pyproject.toml` and is not already on PyPI,
30+
builds the sdist + wheel with `uv build`, and publishes via OIDC with PEP 740
31+
attestations. The `pypi-release` environment prompts for approval first.
32+
4. Verify: `pip install atomicmemory==X.Y.Z` from a clean environment.
33+
34+
The workflow is `workflow_dispatch`-only and guarded to run solely on the public
35+
mirror, so a routine snapshot sync never triggers a publish.

atomicmemory/__init__.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,36 @@
1717
from atomicmemory.core.errors import (
1818
AtomicMemoryError,
1919
ConfigError,
20+
InvalidScopeError,
2021
NetworkError,
2122
NotInitializedError,
2223
ProviderError,
2324
RateLimitError,
25+
UnsupportedOperationError,
2426
ValidationError,
2527
)
28+
from atomicmemory.entities import (
29+
AsyncEntitiesClient,
30+
DeletedCounts,
31+
DeleteEntityResult,
32+
EntitiesClient,
33+
EntitiesClientConfig,
34+
EntitiesClientError,
35+
EntityAttribute,
36+
EntityCard,
37+
EntityDetail,
38+
EntityListResult,
39+
EntityProfile,
40+
EntityProfileBlock,
41+
EntityRelation,
42+
EntitySettings,
43+
EntitySummary,
44+
EntityType,
45+
MemoryHistory,
46+
MemoryHistoryEntry,
47+
MergedCounts,
48+
MergeEntitiesResult,
49+
)
2650
from atomicmemory.memory.capability_profiles import (
2751
CapabilityGap,
2852
CapabilityProfile,
@@ -110,6 +134,7 @@
110134
"ArtifactRange",
111135
"ArtifactRef",
112136
"AsyncAtomicMemoryClient",
137+
"AsyncEntitiesClient",
113138
"AsyncMemoryClient",
114139
"AsyncProviderStatus",
115140
"AsyncStorageClient",
@@ -127,6 +152,21 @@
127152
"DeleteArtifactOptions",
128153
"DeleteArtifactPolicy",
129154
"DeleteArtifactResult",
155+
"DeleteEntityResult",
156+
"DeletedCounts",
157+
"EntitiesClient",
158+
"EntitiesClientConfig",
159+
"EntitiesClientError",
160+
"EntityAttribute",
161+
"EntityCard",
162+
"EntityDetail",
163+
"EntityListResult",
164+
"EntityProfile",
165+
"EntityProfileBlock",
166+
"EntityRelation",
167+
"EntitySettings",
168+
"EntitySummary",
169+
"EntityType",
130170
"FieldFilter",
131171
"FieldFilterOp",
132172
"FilecoinDirectStorageNotSupportedError",
@@ -140,15 +180,20 @@
140180
"IngestInput",
141181
"IngestResult",
142182
"Insight",
183+
"InvalidScopeError",
143184
"ListRequest",
144185
"ListResultPage",
145186
"Memory",
146187
"MemoryClient",
188+
"MemoryHistory",
189+
"MemoryHistoryEntry",
147190
"MemoryKind",
148191
"MemoryNamespaceConfig",
149192
"MemoryRef",
150193
"MemoryVersion",
151194
"MemoryVersionEvent",
195+
"MergeEntitiesResult",
196+
"MergedCounts",
152197
"Message",
153198
"MessageIngest",
154199
"MessageRole",
@@ -179,6 +224,7 @@
179224
"StoredArtifact",
180225
"TextIngest",
181226
"UnsupportedCapabilityError",
227+
"UnsupportedOperationError",
182228
"ValidationError",
183229
"VerbatimIngest",
184230
"VerificationResult",

atomicmemory/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
__version__: The current package version string (PEP 440).
55
"""
66

7-
__version__ = "1.1.0"
7+
__version__ = "1.1.1"

0 commit comments

Comments
 (0)