argus-cortex 0.1.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. argus_cortex-0.3.0/.env.example +21 -0
  2. {argus_cortex-0.1.0 → argus_cortex-0.3.0}/.github/workflows/release.yml +9 -1
  3. {argus_cortex-0.1.0 → argus_cortex-0.3.0}/.gitignore +3 -1
  4. argus_cortex-0.3.0/CHANGELOG.md +77 -0
  5. argus_cortex-0.3.0/CLAUDE.md +48 -0
  6. argus_cortex-0.3.0/PKG-INFO +164 -0
  7. argus_cortex-0.3.0/README.md +121 -0
  8. {argus_cortex-0.1.0 → argus_cortex-0.3.0}/pyproject.toml +23 -2
  9. {argus_cortex-0.1.0 → argus_cortex-0.3.0}/src/argus_cortex/__init__.py +70 -0
  10. {argus_cortex-0.1.0 → argus_cortex-0.3.0}/src/argus_cortex/_version.py +2 -2
  11. argus_cortex-0.3.0/src/argus_cortex/server.py +155 -0
  12. argus_cortex-0.3.0/src/argus_cortex/store/__init__.py +72 -0
  13. argus_cortex-0.3.0/src/argus_cortex/store/blob.py +310 -0
  14. argus_cortex-0.3.0/src/argus_cortex/store/config.py +76 -0
  15. argus_cortex-0.3.0/src/argus_cortex/store/errors.py +82 -0
  16. argus_cortex-0.3.0/src/argus_cortex/store/lineage.py +447 -0
  17. argus_cortex-0.3.0/src/argus_cortex/store/models.py +116 -0
  18. argus_cortex-0.3.0/src/argus_cortex/store/schema.py +79 -0
  19. argus_cortex-0.3.0/src/argus_cortex/store/vector.py +316 -0
  20. argus_cortex-0.3.0/src/argus_cortex/xmp.py +409 -0
  21. argus_cortex-0.3.0/tests/test_blob.py +311 -0
  22. argus_cortex-0.3.0/tests/test_server.py +151 -0
  23. argus_cortex-0.3.0/tests/test_store.py +419 -0
  24. argus_cortex-0.3.0/tests/test_store_pg_integration.py +93 -0
  25. argus_cortex-0.3.0/tests/test_vector.py +335 -0
  26. argus_cortex-0.3.0/tests/test_xmp.py +157 -0
  27. argus_cortex-0.1.0/PKG-INFO +0 -72
  28. argus_cortex-0.1.0/README.md +0 -47
  29. {argus_cortex-0.1.0 → argus_cortex-0.3.0}/.copier-answers.yml +0 -0
  30. {argus_cortex-0.1.0 → argus_cortex-0.3.0}/.github/workflows/ci.yml +0 -0
  31. {argus_cortex-0.1.0 → argus_cortex-0.3.0}/LICENSE +0 -0
  32. {argus_cortex-0.1.0 → argus_cortex-0.3.0}/Makefile +0 -0
  33. {argus_cortex-0.1.0 → argus_cortex-0.3.0}/src/argus_cortex/backends.py +0 -0
  34. {argus_cortex-0.1.0 → argus_cortex-0.3.0}/src/argus_cortex/py.typed +0 -0
  35. {argus_cortex-0.1.0 → argus_cortex-0.3.0}/src/argus_cortex/taxonomy.py +0 -0
  36. {argus_cortex-0.1.0 → argus_cortex-0.3.0}/src/argus_cortex/wire.py +0 -0
  37. {argus_cortex-0.1.0 → argus_cortex-0.3.0}/tests/test_backends.py +0 -0
  38. {argus_cortex-0.1.0 → argus_cortex-0.3.0}/tests/test_smoke.py +0 -0
  39. {argus_cortex-0.1.0 → argus_cortex-0.3.0}/tests/test_taxonomy.py +0 -0
  40. {argus_cortex-0.1.0 → argus_cortex-0.3.0}/tests/test_wire.py +0 -0
@@ -0,0 +1,21 @@
1
+ # Optional stateful services layer (argus_cortex.store).
2
+ # Every value is optional: unset a URL and that feature no-ops (graceful degrade).
3
+ # Copy to .env and fill in only the services you actually run.
4
+
5
+ # Postgres — lineage DAG + human-edit capture (Phase 1).
6
+ # Unset -> open_lineage_store() returns a no-op NullLineageStore.
7
+ CORTEX_PG_URL=postgresql://user:pass@localhost:5432/argus
8
+ # Connection-pool bounds (optional; defaults 0 / 8 — min 0 holds no idle conn).
9
+ CORTEX_PG_POOL_MIN_SIZE=0
10
+ CORTEX_PG_POOL_MAX_SIZE=8
11
+
12
+ # Qdrant — image + tag-set embeddings (Phase 2).
13
+ CORTEX_QDRANT_URL=http://localhost:6333
14
+
15
+ # MinIO / S3 — owned image bytes (Phase 3). Scheme-less endpoint defaults to TLS;
16
+ # write http:// explicitly for a plaintext endpoint. Region matters on AWS S3.
17
+ CORTEX_S3_ENDPOINT=http://localhost:9000
18
+ CORTEX_S3_ACCESS_KEY=
19
+ CORTEX_S3_SECRET_KEY=
20
+ CORTEX_S3_BUCKET=argus
21
+ CORTEX_S3_REGION=us-east-1
@@ -62,7 +62,15 @@ jobs:
62
62
 
63
63
  publish-pypi:
64
64
  runs-on: ubuntu-latest
65
- needs: publish-testpypi
65
+ # Ordered *after* the TestPyPI rehearsal so its outcome is visible first, but
66
+ # deliberately not gated on it. A TestPyPI-only fault — a missing trusted
67
+ # publisher, an index outage — must not silently withhold the real release:
68
+ # that failure mode shipped argus-proof images for two tags with no wheel on
69
+ # PyPI, and nothing surfaced it because publish-image is independent.
70
+ # Only the build + install gate is required; a TestPyPI failure still marks
71
+ # the whole run failed, so the problem stays loud.
72
+ needs: [test-install, publish-testpypi]
73
+ if: "!cancelled() && needs.test-install.result == 'success'"
66
74
  environment: pypi
67
75
  steps:
68
76
  - uses: actions/download-artifact@v4
@@ -6,5 +6,7 @@ build/
6
6
  *.egg-info/
7
7
  .pytest_cache/
8
8
  .ruff_cache/
9
+ # local service config for the optional stores (.env.example stays tracked)
10
+ .env
9
11
  # hatch-vcs writes this at build time (version derived from git tags)
10
- src/argus_core/_version.py
12
+ src/argus_cortex/_version.py
@@ -0,0 +1,77 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.3.0] - 2026-10-25
11
+
12
+ ### Added
13
+ - **`argus_cortex.xmp`** — the suite's one XMP sidecar implementation, hoisted
14
+ from argus-lens so lens (keywords/caption) and argus-curator (face regions,
15
+ content rating) share a writer. `write_xmp()` / `read_xmp()` /
16
+ `merge_xmp()` / `parse_xmp()` / `render_xmp()` / `sidecar_path()` over an
17
+ `XmpData` model, all exported from the top-level package.
18
+ - Namespaces: `dc` (subject, description), `xmp` (Rating, Label), `lr`
19
+ (hierarchicalSubject), `Iptc4xmpExt` (PersonInImage), `mwg-rs` regions
20
+ (`XmpRegions` / `XmpRegion`, MWG centre-based normalised coordinates), and a
21
+ new `argus` namespace for `ContentRating`. The `argus` namespace URI
22
+ (`https://github.com/smk762/argus-cortex/ns/xmp/1.0/`) is now a wire
23
+ contract.
24
+ - **Writes merge**: each property set in `XmpData` replaces that property
25
+ (latest write wins, element or attribute form, across every
26
+ `rdf:Description`); everything else in the sidecar — Lightroom develop
27
+ settings, GPS, another stage's fields, foreign namespace prefixes — is
28
+ preserved. `None` leaves a property untouched, an empty value removes it;
29
+ `replace=True` discards the existing sidecar.
30
+ - Writes are atomic (temp file + rename) and keep the existing file mode
31
+ (`0644` for a new sidecar). An unparseable existing sidecar raises
32
+ `XmpError` and is never clobbered; a DTD / entity declaration is refused.
33
+ - Stdlib `xml.etree` + pydantic only, so the top-level import stays
34
+ pydantic-only.
35
+ - `CLAUDE.md` agent guide.
36
+
37
+ ### Security
38
+ - **eMudhra/emSign CA incident.** The extras that make outbound TLS
39
+ connections (`[remote]` httpx, `[qdrant]`, `[s3]`) now require
40
+ `certifi>=2026.10.19`, which no longer trusts the compromised emSign
41
+ intermediates. See https://discuss.python.org/t/109576.
42
+
43
+ ## [0.2.0] - 2026-07-21
44
+
45
+ ### Added
46
+ - **Optional stateful-services layer** (`argus_cortex.store`), each store opt-in
47
+ via `CORTEX_*` env vars and a `Null*` no-op when unset:
48
+ - Phase 1 — Postgres lineage store (`source_asset → caption → human_edit →
49
+ dataset_membership → training_run`), backed by a psycopg connection pool
50
+ with an atomic `transaction()` boundary (`[postgres]` extra).
51
+ - Phase 2 — Qdrant vector store for image / tag-set embeddings
52
+ (`[qdrant]` extra).
53
+ - Phase 3 — MinIO/S3 content-addressed blob store, keyed on sha256
54
+ (`content_key()` == `source_asset.sha256`) (`[s3]` extra).
55
+ - **`argus_cortex.server`** — shared pure-ASGI `WriteGuard`, the
56
+ `constant_refuse()` / `cross_site_refuse()` predicates, `env_flag()` and the
57
+ `UNSAFE_METHODS` / `TRUTHY` / `FALSY` constants (`[server]` extra).
58
+
59
+ ### Fixed
60
+ - Release: a TestPyPI failure no longer gates the real PyPI release.
61
+
62
+ ## [0.1.0] - 2026-07-08
63
+
64
+ ### Added
65
+ - Initial release (renamed from `argus-core`; that PyPI name was taken):
66
+ - `argus_cortex.taxonomy` — `TargetProfile`, `TargetStyle`, `TargetCategory`.
67
+ - `argus_cortex.wire` — versioned wire-schema toolkit (`make_versioned_base`,
68
+ `check_version` with an injectable error type, `render_schema` /
69
+ `wire_schema`, `VersionError`).
70
+ - `argus_cortex.backends` — the local/remote AI-backend contract
71
+ (`LocalBackend`, `RemoteBackend`, `RemoteProvider`, `with_retries`
72
+ retrying only transient errors, `resolve_device`).
73
+
74
+ [Unreleased]: https://github.com/smk762/argus-cortex/compare/v0.3.0...HEAD
75
+ [0.3.0]: https://github.com/smk762/argus-cortex/compare/v0.2.0...v0.3.0
76
+ [0.2.0]: https://github.com/smk762/argus-cortex/compare/v0.1.0...v0.2.0
77
+ [0.1.0]: https://github.com/smk762/argus-cortex/releases/tag/v0.1.0
@@ -0,0 +1,48 @@
1
+ # CLAUDE.md — argus-cortex
2
+
3
+ Guidance for AI agents working in this repo. Human-facing usage lives in [README.md](README.md); this file is the orientation an agent needs to change code safely.
4
+
5
+ ## What this is
6
+
7
+ The **shared foundation library** of the Argus suite (quarry → curator → lens → forge → proof). It holds the code the stages *share* so it lives in one place instead of copy-pasted per repo: the target taxonomy, the wire-schema versioning discipline, the local/remote AI-backend contract, and the optional stateful-services layer.
8
+
9
+ Naming: the working directory is `argus-core`, but the GitHub repo, the PyPI package, and the import name are all **`argus-cortex`** (`src/argus_cortex/`). Don't "fix" the mismatch.
10
+
11
+ Because everything here is imported by the downstream stages, **a change ripples across the whole suite** — treat this repo as an API, not a leaf. Read [README.md](README.md) for the *why* and worked examples; it is the source of truth for behaviour.
12
+
13
+ ## Layout
14
+
15
+ `src/argus_cortex/` (top level is pydantic-only; everything heavier is behind an extra and lazily imported):
16
+
17
+ - `taxonomy.py` — the "moat" types every stage inherits verbatim: `TargetProfile`, `TargetStyle`/`TargetCategory` literals. Keep it dependency-light; a change here retrains everyone's expectations.
18
+ - `wire.py` — the versioned wire-schema toolkit consumers build their contracts on: `make_versioned_base(field_name, current, majors)` (a Pydantic base that stamps + version-checks a field like `proof_version`/`manifest_version`), `check_version()` (the major-compatibility gate), `render_schema()`/`wire_schema()` (the canonical committed-schema string behind each consumer's `schema --check` CLI), and `VersionError`.
19
+ - `backends.py` — the AI-backend contract: `Backend`/`LocalBackend`/`RemoteBackend` (point at a hosted service via `from_host(host, port)` or `from_provider(RemoteProvider.…)`), plus `with_retries()` and `resolve_device()`. httpx is lazy, behind `[remote]`.
20
+ - `server.py` — the shared FastAPI/ASGI scaffolding other repos' micro-servers mount: `WriteGuard` (pure-ASGI method gate), the `constant_refuse()` (read-only/replay) and `cross_site_refuse()` (cross-origin write) predicates, `env_flag()`, and the `UNSAFE_METHODS`/`TRUTHY`/`FALSY` constants. Starlette is behind `[server]`.
21
+ - `xmp.py` — the suite's XMP sidecar read/merge/write (`write_xmp`/`read_xmp`/`merge_xmp`/`parse_xmp`, `XmpData`/`XmpRegions`, `XmpError`), used by lens (keywords/caption) and curator (faces, content rating). Stdlib `xml.etree` only. **Writes merge — latest wins per property, foreign properties preserved; keep that.** `None` = leave untouched, empty = remove. A DTD is refused and an unparseable sidecar raises rather than being overwritten. The `argus` namespace URI is a wire contract — don't change it.
22
+ - `store/` — the optional stateful-services layer, opt-in per `CORTEX_*` env var: `lineage.py` (Phase 1 Postgres DAG, `open_lineage_store()`), `vector.py` (Phase 2 Qdrant, `open_vector_store()`), `blob.py` (Phase 3 MinIO/S3, `open_blob_store()`), over `config.py`/`models.py`/`errors.py`. Each `open_*()` returns a `Null*` no-op when its URL is unset. Drivers are behind `[postgres]`/`[qdrant]`/`[s3]`.
23
+
24
+ There is **no CLI and no committed schema in this repo** — the `schema`/`schema --check` pattern is something `wire.py` *enables* for downstream repos, not something cortex runs on itself.
25
+
26
+ ## Commands
27
+
28
+ ```bash
29
+ make install # uv venv + editable install with [dev]
30
+ make test # uv run --no-sync pytest --tb=short -q
31
+ make lint # ruff check + format --check (pinned ruff 0.15.16)
32
+ make format # ruff format + check --fix
33
+ ```
34
+
35
+ Run one test: `uv run --no-sync pytest tests/test_wire.py::test_name -q`.
36
+
37
+ ## Conventions & gotchas
38
+
39
+ - **This is shared code — changes ripple.** A `taxonomy.py` edit or a `wire.py`/`backends.py` semantics change lands in every stage on their next bump. Prefer additive, backward-compatible changes; if you break the wire contract, that is a **major** version (`check_version` is what refuses the old major downstream).
40
+ - **Keep the top-level import pydantic-only.** `import argus_cortex` must not pull in starlette/httpx/psycopg/qdrant/minio — those are optional extras, imported lazily inside the module that needs them (see the `try/except ImportError` → "install `argus-cortex[extra]`" pattern). Don't hoist an optional import to module top level.
41
+ - **Stores fail *open* to a no-op, not an error.** `open_*()` returns a `Null*` store when the `CORTEX_*` URL is unset, so a stage with no DB configured still runs. Preserve that; blob identity is content-addressed on sha256 (`content_key()` == `source_asset.sha256`) — storage, dedup, and lineage join share one identity.
42
+ - **`WriteGuard` is pure ASGI on purpose** (not `BaseHTTPMiddleware`), so it never buffers the SSE/NDJSON streams downstream servers return. Register it *before* `CORSMiddleware`. `env_flag()` warns on a mistyped protection flag rather than silently disabling it — keep that fail-safe behaviour.
43
+ - **Versioning is git-tag-derived** (`hatch-vcs`). Never hand-edit a version; `src/argus_cortex/_version.py` is generated (gitignored). Tag `vX.Y.Z` to release.
44
+ - Pydantic v2 everywhere; async tests via `asyncio_mode = auto`. Ruff pinned **0.15.16**, line-length 120, target py311.
45
+
46
+ ## CI / release
47
+
48
+ CI runs via the shared [`argus-ci`](https://github.com/smk762/argus-ci) reusable workflow (ruff + pytest with the `dev` extra — which installs the real psycopg/qdrant/minio/starlette clients so each store's live driver path is exercised, not just fakes). Release publishes to PyPI (OIDC trusted publishing) on `v*` tags — **no container image, this is a library**. Scaffolded from [`argus-pkg-template`](https://github.com/smk762/argus-pkg-template); run `copier update` to pull tooling changes.
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.5
2
+ Name: argus-cortex
3
+ Version: 0.3.0
4
+ Summary: Shared foundation for the Argus suite: taxonomy, versioned wire-schema tooling, and the local/remote AI backend contract
5
+ Project-URL: Repository, https://github.com/smk762/argus-cortex
6
+ Author: smk762
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Typing :: Typed
16
+ Requires-Python: >=3.11
17
+ Requires-Dist: pydantic>=2.7
18
+ Provides-Extra: dev
19
+ Requires-Dist: certifi>=2026.10.19; extra == 'dev'
20
+ Requires-Dist: httpx2>=2.7; extra == 'dev'
21
+ Requires-Dist: httpx>=0.27; extra == 'dev'
22
+ Requires-Dist: minio>=7.2; extra == 'dev'
23
+ Requires-Dist: psycopg[binary,pool]>=3.1; extra == 'dev'
24
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
25
+ Requires-Dist: pytest>=8.0; extra == 'dev'
26
+ Requires-Dist: qdrant-client>=1.10; extra == 'dev'
27
+ Requires-Dist: ruff==0.15.16; extra == 'dev'
28
+ Requires-Dist: starlette>=0.37; extra == 'dev'
29
+ Provides-Extra: postgres
30
+ Requires-Dist: psycopg[binary,pool]>=3.1; extra == 'postgres'
31
+ Provides-Extra: qdrant
32
+ Requires-Dist: certifi>=2026.10.19; extra == 'qdrant'
33
+ Requires-Dist: qdrant-client>=1.10; extra == 'qdrant'
34
+ Provides-Extra: remote
35
+ Requires-Dist: certifi>=2026.10.19; extra == 'remote'
36
+ Requires-Dist: httpx>=0.27; extra == 'remote'
37
+ Provides-Extra: s3
38
+ Requires-Dist: certifi>=2026.10.19; extra == 's3'
39
+ Requires-Dist: minio>=7.2; extra == 's3'
40
+ Provides-Extra: server
41
+ Requires-Dist: starlette>=0.37; extra == 'server'
42
+ Description-Content-Type: text/markdown
43
+
44
+ # argus-cortex
45
+
46
+ Shared foundation for the Argus suite: taxonomy, versioned wire-schema tooling, and the local/remote AI backend contract.
47
+
48
+ Part of the [Argus suite](https://github.com/smk762?tab=repositories&q=argus) (quarry → curator → lens → forge → proof). This is the library the stages import so the code they *share* — the target taxonomy, the wire-schema versioning discipline, and the way they call local or hosted models — lives in one place instead of being copy-pasted per repo.
49
+
50
+ ## What's here
51
+
52
+ - **`argus_cortex.taxonomy`** — `TargetProfile` / `TargetStyle` / `TargetCategory`, the "moat" types every stage inherits verbatim.
53
+ - **`argus_cortex.wire`** — the versioned wire-schema toolkit: `check_version()` (major-compatibility gate), `make_versioned_base()` (a Pydantic base that stamps + checks a version field like `proof_version` / `manifest_version`), and `wire_schema()` / `render_schema()` for the committed-schema `schema --check` CLI pattern.
54
+ - **`argus_cortex.backends`** — the AI-backend contract generalised from argus-lens: `Backend` / `LocalBackend` / `RemoteBackend` (point at a hosted service by `base_url`, `host`/`port`, or a known `RemoteProvider`), plus `with_retries()` and `resolve_device()`. httpx lives behind the `[remote]` extra.
55
+ - **`argus_cortex.server`** — shared write-guard + env-flag scaffolding for the suite's FastAPI micro-servers: `WriteGuard` (a pure-ASGI method-gate that never buffers streaming responses), the `constant_refuse()` / `cross_site_refuse()` predicates (read-only/replay mode and cross-site-write protection are two configs of the one guard), the `UNSAFE_METHODS` / `TRUTHY` / `FALSY` constants, and `env_flag()` (warns on a mistyped protection flag rather than silently disabling it). Starlette lives behind the `[server]` extra.
56
+ - **`argus_cortex.xmp`** — the suite's one XMP sidecar implementation (hoisted from argus-lens): `write_xmp()` / `read_xmp()` / `merge_xmp()` / `parse_xmp()` over an `XmpData` model covering `dc` (keywords, description), `xmp` (Rating, Label), `lr` (hierarchical keywords), `Iptc4xmpExt` (PersonInImage), `mwg-rs` face/pet regions, and `argus:ContentRating`. Writes **merge**: each property you set replaces that property (latest write wins), everything else in the sidecar — Lightroom develop settings, GPS, another stage's fields — is preserved. `None` leaves a property alone, an empty value removes it. Writes are atomic; an unparseable existing sidecar raises `XmpError` instead of being clobbered. Stdlib only.
57
+ - **`argus_cortex.store`** — the optional **stateful services layer** (the suite's persistent "memory"). External and opt-in — every store is configured by `CORTEX_*` env vars and no-ops when its URL is unset.
58
+ - **Phase 1 — Postgres lineage store.** Persists the `source_asset → caption → human_edit → dataset_membership → training_run` DAG for LoRA reproducibility, edit capture, and the feedback loop. Backed by a psycopg connection **pool** (concurrent `asyncio.to_thread` calls are safe; a dropped connection is replaced), with a `transaction()` boundary for atomic multi-step writes. `open_lineage_store()` returns a no-op store when `CORTEX_PG_URL` is unset; psycopg lives behind the `[postgres]` extra.
59
+ - **Phase 2 — Qdrant vector store.** Stores image + tag-set embeddings for retrieval-augmented few-shot and near-duplicate curation; put a `caption_id`/`asset_id` in a point's payload and a search hit joins straight back to the lineage. `open_vector_store()` no-ops when `CORTEX_QDRANT_URL` is unset; qdrant-client lives behind the `[qdrant]` extra. cortex stores vectors it's *handed* — computing embeddings is the caller's job.
60
+ - **Phase 3 — MinIO/S3 blob store.** Owns image bytes when cortex can't rely on the filesystem/Immich (exported/selected training images). Blobs are content-addressed on sha256 (`content_key()` == `source_asset.sha256`), so storage, dedup, and lineage join share one identity. `open_blob_store()` no-ops when `CORTEX_S3_ENDPOINT`/`CORTEX_S3_BUCKET` are unset; the `minio` client lives behind the `[s3]` extra.
61
+
62
+ ```python
63
+ from argus_cortex.wire import make_versioned_base, render_schema
64
+ from argus_cortex.backends import RemoteBackend, RemoteProvider
65
+
66
+ # per-package versioned base (readable field name kept local, logic shared)
67
+ _Versioned = make_versioned_base("proof_version", "1.0", ("1",))
68
+
69
+ # reach a hosted scorer by IP/port, or a known provider
70
+ scorer = RemoteBackend.from_host("192.168.1.20", 9000, api_key="…")
71
+ nim = RemoteBackend.from_provider(RemoteProvider.NVIDIA_NIM, api_key="…")
72
+ ```
73
+
74
+ ```python
75
+ from argus_cortex.xmp import XmpData, XmpRegion, XmpRegions, write_xmp
76
+
77
+ # lens writes captions/keywords …
78
+ write_xmp("pics/beach.jpg", XmpData(keywords=["beach", "sunset"], description="a beach at sunset"))
79
+ # … curator later adds faces + a content rating to the same sidecar; lens's fields survive
80
+ write_xmp(
81
+ "pics/beach.jpg",
82
+ XmpData(
83
+ persons=["Ada"],
84
+ regions=XmpRegions(width=4000, height=3000, regions=[XmpRegion(x=0.52, y=0.4, w=0.1, h=0.18, name="Ada")]),
85
+ content_rating="safe",
86
+ ),
87
+ )
88
+ ```
89
+
90
+ ```python
91
+ from argus_cortex.store import open_lineage_store, SourceAsset, Caption, HumanEdit
92
+
93
+ # no-op if CORTEX_PG_URL is unset; PostgresLineageStore if it's set
94
+ store = open_lineage_store() # reads CORTEX_* from the environment
95
+ store.ensure_schema() # idempotent bootstrap of the lineage tables
96
+
97
+ # one atomic unit — the asset + caption + edit commit together or not at all
98
+ with store.transaction():
99
+ asset_id = store.record_asset(SourceAsset(uri="immich://…", sha256="…"))
100
+ # lens emits a CaptionResult; cortex ingests it (cortex never imports lens)
101
+ cap_id = store.record_caption(asset_id, Caption.from_caption_result(result, profile={...}))
102
+ store.record_edit(cap_id, HumanEdit(edited_caption="…", editor="alice"))
103
+
104
+ # the (model_caption, edited_caption) pairs the summariser feedback loop trains on
105
+ pairs = store.caption_edit_pairs()
106
+ ```
107
+
108
+ ```python
109
+ from argus_cortex.store import open_vector_store, IMAGE_COLLECTION
110
+
111
+ # no-op if CORTEX_QDRANT_URL is unset; QdrantVectorStore if it's set
112
+ vec = open_vector_store()
113
+ vec.ensure_collection(IMAGE_COLLECTION, dim=512) # idempotent
114
+
115
+ # store an embedding you computed, tagged with its lineage id
116
+ vec.upsert(IMAGE_COLLECTION, point_id=cap_id, vector=embedding, payload={"caption_id": cap_id})
117
+
118
+ # retrieve similar past captions to steer the summariser (few-shot)
119
+ hits = vec.search(IMAGE_COLLECTION, vector=query_embedding, top_k=5)
120
+ similar_caption_ids = [h.payload["caption_id"] for h in hits] # join back to Postgres
121
+ ```
122
+
123
+ ```python
124
+ from argus_cortex.store import open_blob_store
125
+
126
+ # no-op if CORTEX_S3_ENDPOINT / CORTEX_S3_BUCKET are unset; S3BlobStore if set
127
+ blobs = open_blob_store()
128
+ blobs.ensure_bucket()
129
+
130
+ # content-addressed: the returned key IS the sha256 (== source_asset.sha256)
131
+ sha = blobs.put_content_addressed(image_bytes, content_type="image/jpeg")
132
+ data = blobs.get(sha) # bytes, or None if absent
133
+ link = blobs.url(sha, expires=3600) # presigned GET URL
134
+ ```
135
+
136
+ ## Install
137
+
138
+ ```bash
139
+ uv pip install argus-cortex # core (pydantic only)
140
+ uv pip install "argus-cortex[remote]" # + httpx for RemoteBackend
141
+ uv pip install "argus-cortex[server]" # + starlette for argus_cortex.server (WriteGuard, env_flag)
142
+ uv pip install "argus-cortex[postgres]" # + psycopg for the lineage store
143
+ uv pip install "argus-cortex[qdrant]" # + qdrant-client for the vector store
144
+ uv pip install "argus-cortex[s3]" # + minio for the blob store
145
+ ```
146
+
147
+ The stateful services are configured via `CORTEX_*` env vars — see [`.env.example`](.env.example). Each is optional: leave a URL unset and that feature degrades to a no-op.
148
+
149
+ ## Develop
150
+
151
+ ```bash
152
+ make install # venv + editable install with the "dev" extras
153
+ make test
154
+ make lint
155
+ ```
156
+
157
+ ## CI / Release
158
+
159
+ - **CI** runs via the shared [`argus-ci`](https://github.com/smk762/argus-ci) reusable workflow.
160
+ - **Release** publishes to PyPI (OIDC trusted publishing) on `v*` tags. No container image — this is a library.
161
+ - Versioning is derived from git tags via `hatch-vcs` — tag `vX.Y.Z` to cut a release.
162
+
163
+ This repo was scaffolded from [`argus-pkg-template`](https://github.com/smk762/argus-pkg-template).
164
+ Run `copier update` to pull template changes (CI, release, tooling).
@@ -0,0 +1,121 @@
1
+ # argus-cortex
2
+
3
+ Shared foundation for the Argus suite: taxonomy, versioned wire-schema tooling, and the local/remote AI backend contract.
4
+
5
+ Part of the [Argus suite](https://github.com/smk762?tab=repositories&q=argus) (quarry → curator → lens → forge → proof). This is the library the stages import so the code they *share* — the target taxonomy, the wire-schema versioning discipline, and the way they call local or hosted models — lives in one place instead of being copy-pasted per repo.
6
+
7
+ ## What's here
8
+
9
+ - **`argus_cortex.taxonomy`** — `TargetProfile` / `TargetStyle` / `TargetCategory`, the "moat" types every stage inherits verbatim.
10
+ - **`argus_cortex.wire`** — the versioned wire-schema toolkit: `check_version()` (major-compatibility gate), `make_versioned_base()` (a Pydantic base that stamps + checks a version field like `proof_version` / `manifest_version`), and `wire_schema()` / `render_schema()` for the committed-schema `schema --check` CLI pattern.
11
+ - **`argus_cortex.backends`** — the AI-backend contract generalised from argus-lens: `Backend` / `LocalBackend` / `RemoteBackend` (point at a hosted service by `base_url`, `host`/`port`, or a known `RemoteProvider`), plus `with_retries()` and `resolve_device()`. httpx lives behind the `[remote]` extra.
12
+ - **`argus_cortex.server`** — shared write-guard + env-flag scaffolding for the suite's FastAPI micro-servers: `WriteGuard` (a pure-ASGI method-gate that never buffers streaming responses), the `constant_refuse()` / `cross_site_refuse()` predicates (read-only/replay mode and cross-site-write protection are two configs of the one guard), the `UNSAFE_METHODS` / `TRUTHY` / `FALSY` constants, and `env_flag()` (warns on a mistyped protection flag rather than silently disabling it). Starlette lives behind the `[server]` extra.
13
+ - **`argus_cortex.xmp`** — the suite's one XMP sidecar implementation (hoisted from argus-lens): `write_xmp()` / `read_xmp()` / `merge_xmp()` / `parse_xmp()` over an `XmpData` model covering `dc` (keywords, description), `xmp` (Rating, Label), `lr` (hierarchical keywords), `Iptc4xmpExt` (PersonInImage), `mwg-rs` face/pet regions, and `argus:ContentRating`. Writes **merge**: each property you set replaces that property (latest write wins), everything else in the sidecar — Lightroom develop settings, GPS, another stage's fields — is preserved. `None` leaves a property alone, an empty value removes it. Writes are atomic; an unparseable existing sidecar raises `XmpError` instead of being clobbered. Stdlib only.
14
+ - **`argus_cortex.store`** — the optional **stateful services layer** (the suite's persistent "memory"). External and opt-in — every store is configured by `CORTEX_*` env vars and no-ops when its URL is unset.
15
+ - **Phase 1 — Postgres lineage store.** Persists the `source_asset → caption → human_edit → dataset_membership → training_run` DAG for LoRA reproducibility, edit capture, and the feedback loop. Backed by a psycopg connection **pool** (concurrent `asyncio.to_thread` calls are safe; a dropped connection is replaced), with a `transaction()` boundary for atomic multi-step writes. `open_lineage_store()` returns a no-op store when `CORTEX_PG_URL` is unset; psycopg lives behind the `[postgres]` extra.
16
+ - **Phase 2 — Qdrant vector store.** Stores image + tag-set embeddings for retrieval-augmented few-shot and near-duplicate curation; put a `caption_id`/`asset_id` in a point's payload and a search hit joins straight back to the lineage. `open_vector_store()` no-ops when `CORTEX_QDRANT_URL` is unset; qdrant-client lives behind the `[qdrant]` extra. cortex stores vectors it's *handed* — computing embeddings is the caller's job.
17
+ - **Phase 3 — MinIO/S3 blob store.** Owns image bytes when cortex can't rely on the filesystem/Immich (exported/selected training images). Blobs are content-addressed on sha256 (`content_key()` == `source_asset.sha256`), so storage, dedup, and lineage join share one identity. `open_blob_store()` no-ops when `CORTEX_S3_ENDPOINT`/`CORTEX_S3_BUCKET` are unset; the `minio` client lives behind the `[s3]` extra.
18
+
19
+ ```python
20
+ from argus_cortex.wire import make_versioned_base, render_schema
21
+ from argus_cortex.backends import RemoteBackend, RemoteProvider
22
+
23
+ # per-package versioned base (readable field name kept local, logic shared)
24
+ _Versioned = make_versioned_base("proof_version", "1.0", ("1",))
25
+
26
+ # reach a hosted scorer by IP/port, or a known provider
27
+ scorer = RemoteBackend.from_host("192.168.1.20", 9000, api_key="…")
28
+ nim = RemoteBackend.from_provider(RemoteProvider.NVIDIA_NIM, api_key="…")
29
+ ```
30
+
31
+ ```python
32
+ from argus_cortex.xmp import XmpData, XmpRegion, XmpRegions, write_xmp
33
+
34
+ # lens writes captions/keywords …
35
+ write_xmp("pics/beach.jpg", XmpData(keywords=["beach", "sunset"], description="a beach at sunset"))
36
+ # … curator later adds faces + a content rating to the same sidecar; lens's fields survive
37
+ write_xmp(
38
+ "pics/beach.jpg",
39
+ XmpData(
40
+ persons=["Ada"],
41
+ regions=XmpRegions(width=4000, height=3000, regions=[XmpRegion(x=0.52, y=0.4, w=0.1, h=0.18, name="Ada")]),
42
+ content_rating="safe",
43
+ ),
44
+ )
45
+ ```
46
+
47
+ ```python
48
+ from argus_cortex.store import open_lineage_store, SourceAsset, Caption, HumanEdit
49
+
50
+ # no-op if CORTEX_PG_URL is unset; PostgresLineageStore if it's set
51
+ store = open_lineage_store() # reads CORTEX_* from the environment
52
+ store.ensure_schema() # idempotent bootstrap of the lineage tables
53
+
54
+ # one atomic unit — the asset + caption + edit commit together or not at all
55
+ with store.transaction():
56
+ asset_id = store.record_asset(SourceAsset(uri="immich://…", sha256="…"))
57
+ # lens emits a CaptionResult; cortex ingests it (cortex never imports lens)
58
+ cap_id = store.record_caption(asset_id, Caption.from_caption_result(result, profile={...}))
59
+ store.record_edit(cap_id, HumanEdit(edited_caption="…", editor="alice"))
60
+
61
+ # the (model_caption, edited_caption) pairs the summariser feedback loop trains on
62
+ pairs = store.caption_edit_pairs()
63
+ ```
64
+
65
+ ```python
66
+ from argus_cortex.store import open_vector_store, IMAGE_COLLECTION
67
+
68
+ # no-op if CORTEX_QDRANT_URL is unset; QdrantVectorStore if it's set
69
+ vec = open_vector_store()
70
+ vec.ensure_collection(IMAGE_COLLECTION, dim=512) # idempotent
71
+
72
+ # store an embedding you computed, tagged with its lineage id
73
+ vec.upsert(IMAGE_COLLECTION, point_id=cap_id, vector=embedding, payload={"caption_id": cap_id})
74
+
75
+ # retrieve similar past captions to steer the summariser (few-shot)
76
+ hits = vec.search(IMAGE_COLLECTION, vector=query_embedding, top_k=5)
77
+ similar_caption_ids = [h.payload["caption_id"] for h in hits] # join back to Postgres
78
+ ```
79
+
80
+ ```python
81
+ from argus_cortex.store import open_blob_store
82
+
83
+ # no-op if CORTEX_S3_ENDPOINT / CORTEX_S3_BUCKET are unset; S3BlobStore if set
84
+ blobs = open_blob_store()
85
+ blobs.ensure_bucket()
86
+
87
+ # content-addressed: the returned key IS the sha256 (== source_asset.sha256)
88
+ sha = blobs.put_content_addressed(image_bytes, content_type="image/jpeg")
89
+ data = blobs.get(sha) # bytes, or None if absent
90
+ link = blobs.url(sha, expires=3600) # presigned GET URL
91
+ ```
92
+
93
+ ## Install
94
+
95
+ ```bash
96
+ uv pip install argus-cortex # core (pydantic only)
97
+ uv pip install "argus-cortex[remote]" # + httpx for RemoteBackend
98
+ uv pip install "argus-cortex[server]" # + starlette for argus_cortex.server (WriteGuard, env_flag)
99
+ uv pip install "argus-cortex[postgres]" # + psycopg for the lineage store
100
+ uv pip install "argus-cortex[qdrant]" # + qdrant-client for the vector store
101
+ uv pip install "argus-cortex[s3]" # + minio for the blob store
102
+ ```
103
+
104
+ The stateful services are configured via `CORTEX_*` env vars — see [`.env.example`](.env.example). Each is optional: leave a URL unset and that feature degrades to a no-op.
105
+
106
+ ## Develop
107
+
108
+ ```bash
109
+ make install # venv + editable install with the "dev" extras
110
+ make test
111
+ make lint
112
+ ```
113
+
114
+ ## CI / Release
115
+
116
+ - **CI** runs via the shared [`argus-ci`](https://github.com/smk762/argus-ci) reusable workflow.
117
+ - **Release** publishes to PyPI (OIDC trusted publishing) on `v*` tags. No container image — this is a library.
118
+ - Versioning is derived from git tags via `hatch-vcs` — tag `vX.Y.Z` to cut a release.
119
+
120
+ This repo was scaffolded from [`argus-pkg-template`](https://github.com/smk762/argus-pkg-template).
121
+ Run `copier update` to pull template changes (CI, release, tooling).
@@ -24,8 +24,29 @@ dependencies = [
24
24
  ]
25
25
 
26
26
  [project.optional-dependencies]
27
- remote = ["httpx>=0.27"]
28
- dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "ruff==0.15.16"]
27
+ remote = ["httpx>=0.27", "certifi>=2026.10.19"]
28
+ postgres = ["psycopg[binary,pool]>=3.1"]
29
+ qdrant = ["qdrant-client>=1.10", "certifi>=2026.10.19"]
30
+ s3 = ["minio>=7.2", "certifi>=2026.10.19"]
31
+ # The shared ASGI write-guard + env-flag scaffolding for the suite's servers
32
+ # (argus_cortex.server). Starlette ships with FastAPI, so consumers already have
33
+ # it via their own [server] extra.
34
+ server = ["starlette>=0.37"]
35
+ # The store drivers are in dev so each store's real client path (pool/enum/client
36
+ # construction, error wrapping) is exercised in CI, not just via fakes.
37
+ # starlette + httpx power the pure-ASGI guard tests (Starlette TestClient).
38
+ dev = [
39
+ "pytest>=8.0",
40
+ "pytest-asyncio>=0.23",
41
+ "ruff==0.15.16",
42
+ "psycopg[binary,pool]>=3.1",
43
+ "qdrant-client>=1.10",
44
+ "minio>=7.2",
45
+ "starlette>=0.37",
46
+ "httpx>=0.27",
47
+ "httpx2>=2.7",
48
+ "certifi>=2026.10.19",
49
+ ]
29
50
 
30
51
  [project.urls]
31
52
  Repository = "https://github.com/smk762/argus-cortex"
@@ -23,6 +23,30 @@ from argus_cortex.backends import (
23
23
  resolve_device,
24
24
  with_retries,
25
25
  )
26
+ from argus_cortex.store import (
27
+ IMAGE_COLLECTION,
28
+ TAGSET_COLLECTION,
29
+ BlobStore,
30
+ Caption,
31
+ HumanEdit,
32
+ LineageStore,
33
+ NullBlobStore,
34
+ NullLineageStore,
35
+ NullVectorStore,
36
+ PostgresLineageStore,
37
+ QdrantVectorStore,
38
+ S3BlobStore,
39
+ SourceAsset,
40
+ StoreConfig,
41
+ StoreError,
42
+ TrainingRun,
43
+ VectorHit,
44
+ VectorStore,
45
+ content_key,
46
+ open_blob_store,
47
+ open_lineage_store,
48
+ open_vector_store,
49
+ )
26
50
  from argus_cortex.taxonomy import TargetCategory, TargetProfile, TargetStyle
27
51
  from argus_cortex.wire import (
28
52
  VersionError,
@@ -32,6 +56,18 @@ from argus_cortex.wire import (
32
56
  schema_major,
33
57
  wire_schema,
34
58
  )
59
+ from argus_cortex.xmp import (
60
+ XmpData,
61
+ XmpError,
62
+ XmpRegion,
63
+ XmpRegions,
64
+ merge_xmp,
65
+ parse_xmp,
66
+ read_xmp,
67
+ render_xmp,
68
+ sidecar_path,
69
+ write_xmp,
70
+ )
35
71
 
36
72
  __all__ = [
37
73
  "__version__",
@@ -46,6 +82,17 @@ __all__ = [
46
82
  "render_schema",
47
83
  "schema_major",
48
84
  "wire_schema",
85
+ # xmp sidecars
86
+ "XmpData",
87
+ "XmpError",
88
+ "XmpRegion",
89
+ "XmpRegions",
90
+ "merge_xmp",
91
+ "parse_xmp",
92
+ "read_xmp",
93
+ "render_xmp",
94
+ "sidecar_path",
95
+ "write_xmp",
49
96
  # backends
50
97
  "Backend",
51
98
  "BackendError",
@@ -55,4 +102,27 @@ __all__ = [
55
102
  "RemoteProvider",
56
103
  "resolve_device",
57
104
  "with_retries",
105
+ # store (optional stateful services layer)
106
+ "StoreConfig",
107
+ "StoreError",
108
+ "LineageStore",
109
+ "NullLineageStore",
110
+ "PostgresLineageStore",
111
+ "open_lineage_store",
112
+ "Caption",
113
+ "HumanEdit",
114
+ "SourceAsset",
115
+ "TrainingRun",
116
+ "VectorStore",
117
+ "NullVectorStore",
118
+ "QdrantVectorStore",
119
+ "VectorHit",
120
+ "open_vector_store",
121
+ "IMAGE_COLLECTION",
122
+ "TAGSET_COLLECTION",
123
+ "BlobStore",
124
+ "NullBlobStore",
125
+ "S3BlobStore",
126
+ "open_blob_store",
127
+ "content_key",
58
128
  ]
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.0'
22
- __version_tuple__ = version_tuple = (0, 1, 0)
21
+ __version__ = version = '0.3.0'
22
+ __version_tuple__ = version_tuple = (0, 3, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None