argus-cortex 0.4.3__tar.gz → 0.4.4__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.
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/CHANGELOG.md +16 -1
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/PKG-INFO +1 -1
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/_version.py +2 -2
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/moderation/singguard.py +3 -1
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/store/lineage.py +15 -5
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_moderation.py +18 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_store.py +5 -3
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_store_pg_integration.py +14 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/.copier-answers.yml +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/.env.example +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/.github/workflows/ci.yml +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/.github/workflows/release.yml +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/.gitignore +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/CLAUDE.md +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/LICENSE +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/Makefile +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/README.md +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/pyproject.toml +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/__init__.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/backends.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/gpu.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/moderation/__init__.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/moderation/_http.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/moderation/nudenet.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/moderation/policies.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/py.typed +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/server.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/store/__init__.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/store/blob.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/store/config.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/store/errors.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/store/models.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/store/schema.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/store/vector.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/taxonomy.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/wire.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/xmp.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_backends.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_blob.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_gpu.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_policies.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_server.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_smoke.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_taxonomy.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_vector.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_wire.py +0 -0
- {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_xmp.py +0 -0
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.4] - 2026-10-31
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`SingGuardClient` keeps a longer writeup.** `to_result` clipped `explanation` at 4000
|
|
14
|
+
characters, which cut cascade/slow reasoning mid-sentence. The cap is now 16384, enough
|
|
15
|
+
for a 2048-token slow writeup plus a cascade preface.
|
|
16
|
+
- **Re-recording an asset no longer erases what another stage recorded on it.** `record_asset`'s
|
|
17
|
+
upsert on `sha256` replaced `immich_id` and `metadata` outright, so argus-lens recording a caption
|
|
18
|
+
for an image argus-quarry had already recorded (licence, author, source) wiped that provenance,
|
|
19
|
+
and a call without an `immich_id` cleared the stored one. `metadata` now merges key by key
|
|
20
|
+
(`jsonb ||`: the new call's keys win, others are kept) and a `NULL` `immich_id` keeps the stored
|
|
21
|
+
one. `uri` is still last-write-wins: it is the asset's latest location. A caller that needs to
|
|
22
|
+
remove a metadata key can no longer do it through `record_asset`.
|
|
23
|
+
|
|
10
24
|
## [0.4.3] - 2026-10-30
|
|
11
25
|
|
|
12
26
|
### Added
|
|
@@ -164,7 +178,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
164
178
|
(`LocalBackend`, `RemoteBackend`, `RemoteProvider`, `with_retries`
|
|
165
179
|
retrying only transient errors, `resolve_device`).
|
|
166
180
|
|
|
167
|
-
[Unreleased]: https://github.com/smk762/argus-cortex/compare/v0.4.
|
|
181
|
+
[Unreleased]: https://github.com/smk762/argus-cortex/compare/v0.4.4...HEAD
|
|
182
|
+
[0.4.4]: https://github.com/smk762/argus-cortex/compare/v0.4.3...v0.4.4
|
|
168
183
|
[0.4.3]: https://github.com/smk762/argus-cortex/compare/v0.4.2...v0.4.3
|
|
169
184
|
[0.4.2]: https://github.com/smk762/argus-cortex/compare/v0.4.1...v0.4.2
|
|
170
185
|
[0.4.1]: https://github.com/smk762/argus-cortex/compare/v0.4.0...v0.4.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: argus-cortex
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Shared foundation for the Argus suite: taxonomy, versioned wire-schema tooling, and the local/remote AI backend contract
|
|
5
5
|
Project-URL: Repository, https://github.com/smk762/argus-cortex
|
|
6
6
|
Author: smk762
|
|
@@ -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.4.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 4,
|
|
21
|
+
__version__ = version = '0.4.4'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 4, 4)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -47,7 +47,9 @@ MAX_EDGE = 1536
|
|
|
47
47
|
MAX_BYTES = 4 * 1024 * 1024
|
|
48
48
|
_MIN_EDGE = 256 # below this, give up rather than send SingGuard a postage stamp
|
|
49
49
|
_NEEDS_PILLOW = "downscaling images needs: pip install argus-cortex[images] (or pass downscale=False)"
|
|
50
|
-
|
|
50
|
+
# Fits a 2048-token slow writeup plus a short cascade preface. 4000
|
|
51
|
+
# cut recorded reasoning mid-sentence on cascade scans.
|
|
52
|
+
_EXPLANATION_MAX = 16384
|
|
51
53
|
_FIELD_MAX = 200
|
|
52
54
|
|
|
53
55
|
|
|
@@ -70,7 +70,13 @@ class LineageStore(Protocol):
|
|
|
70
70
|
...
|
|
71
71
|
|
|
72
72
|
def record_asset(self, asset: SourceAsset) -> str | None:
|
|
73
|
-
"""Insert (or reuse, by ``sha256``) a source asset; return its id.
|
|
73
|
+
"""Insert (or reuse, by ``sha256``) a source asset; return its id.
|
|
74
|
+
|
|
75
|
+
On reuse, ``uri`` becomes this call's; ``metadata`` merges into the
|
|
76
|
+
stored bag (this call's keys win); a ``None`` ``immich_id`` keeps the
|
|
77
|
+
stored one. So stages recording the same bytes add to the row rather
|
|
78
|
+
than erase each other's fields.
|
|
79
|
+
"""
|
|
74
80
|
...
|
|
75
81
|
|
|
76
82
|
def record_caption(self, asset_id: str, caption: Caption) -> str | None:
|
|
@@ -302,8 +308,12 @@ class PostgresLineageStore:
|
|
|
302
308
|
|
|
303
309
|
def record_asset(self, asset: SourceAsset) -> str | None:
|
|
304
310
|
# De-dupe on content hash: re-ingesting identical bytes returns the
|
|
305
|
-
# existing row (DO UPDATE ensures RETURNING fires even on conflict)
|
|
306
|
-
#
|
|
311
|
+
# existing row (DO UPDATE ensures RETURNING fires even on conflict).
|
|
312
|
+
# Several stages record the same asset (quarry with provenance, lens
|
|
313
|
+
# with a caption), so a later call must not erase what an earlier one
|
|
314
|
+
# knew: uri is the latest location (last-write-wins), immich_id keeps
|
|
315
|
+
# the stored id when this call has none, and metadata merges key by
|
|
316
|
+
# key (jsonb ||: this call's keys win, the others are kept).
|
|
307
317
|
# NOTE: dedup only works when sha256 is set — NULLs are distinct under the
|
|
308
318
|
# UNIQUE index, so a hash-less asset inserts a fresh row every call.
|
|
309
319
|
return self._insert(
|
|
@@ -312,8 +322,8 @@ class PostgresLineageStore:
|
|
|
312
322
|
VALUES (%s, %s, %s, %s::jsonb)
|
|
313
323
|
ON CONFLICT (sha256) DO UPDATE
|
|
314
324
|
SET uri = EXCLUDED.uri,
|
|
315
|
-
immich_id = EXCLUDED.immich_id,
|
|
316
|
-
metadata = EXCLUDED.metadata
|
|
325
|
+
immich_id = COALESCE(EXCLUDED.immich_id, source_asset.immich_id),
|
|
326
|
+
metadata = source_asset.metadata || EXCLUDED.metadata
|
|
317
327
|
RETURNING id
|
|
318
328
|
""",
|
|
319
329
|
(asset.uri, asset.sha256, asset.immich_id, self._json(asset.metadata)),
|
|
@@ -253,6 +253,24 @@ def test_singguard_config() -> None:
|
|
|
253
253
|
SingGuardClient("gopher://g")
|
|
254
254
|
|
|
255
255
|
|
|
256
|
+
def test_to_result_keeps_a_long_explanation() -> None:
|
|
257
|
+
writeup = "Cascade: fast flagged this; the slow pass is the verdict.\n\n" + ("A posed adult. " * 400)
|
|
258
|
+
writeup = writeup.strip()
|
|
259
|
+
assert 4000 < len(writeup) < singguard._EXPLANATION_MAX
|
|
260
|
+
result = singguard.to_result(
|
|
261
|
+
{"safe": True, "verdict": "safe", "rule": "Safe", "explanation": writeup, "categories": []}
|
|
262
|
+
)
|
|
263
|
+
assert result.explanation == writeup
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_to_result_clips_explanation_at_max() -> None:
|
|
267
|
+
exact = "y" * singguard._EXPLANATION_MAX
|
|
268
|
+
over = exact + "z"
|
|
269
|
+
payload = {"safe": True, "verdict": "safe", "rule": "Safe", "categories": []}
|
|
270
|
+
assert singguard.to_result({**payload, "explanation": exact}).explanation == exact
|
|
271
|
+
assert singguard.to_result({**payload, "explanation": over}).explanation == exact
|
|
272
|
+
|
|
273
|
+
|
|
256
274
|
def test_singguard_check(singguard_server) -> None:
|
|
257
275
|
fake, url = singguard_server
|
|
258
276
|
client = SingGuardClient(url, timeout=5, thinking="fast-slow", policy="### A. Minors")
|
|
@@ -206,9 +206,11 @@ def test_record_asset_returns_id_and_upserts_on_sha() -> None:
|
|
|
206
206
|
sql, params = conn.executed[-1]
|
|
207
207
|
assert "INSERT INTO source_asset" in sql
|
|
208
208
|
assert "ON CONFLICT (sha256)" in sql
|
|
209
|
-
# on a dedup hit
|
|
210
|
-
|
|
211
|
-
assert "
|
|
209
|
+
# on a dedup hit uri is refreshed, but a call without an immich_id or a
|
|
210
|
+
# metadata key keeps the stored one (another stage may have recorded it)
|
|
211
|
+
assert "uri = EXCLUDED.uri" in sql
|
|
212
|
+
assert "immich_id = COALESCE(EXCLUDED.immich_id, source_asset.immich_id)" in sql
|
|
213
|
+
assert "metadata = source_asset.metadata || EXCLUDED.metadata" in sql
|
|
212
214
|
# metadata is serialised to a JSON string for the ::jsonb cast
|
|
213
215
|
assert params == ("immich://1", "abc", None, '{"k": "v"}')
|
|
214
216
|
|
|
@@ -81,6 +81,20 @@ def test_transaction_rolls_back_on_error(store: PostgresLineageStore) -> None:
|
|
|
81
81
|
assert reused is not None
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
def test_reupsert_merges_metadata_and_keeps_immich_id(store: PostgresLineageStore) -> None:
|
|
85
|
+
# quarry records provenance first; lens later records the same bytes with
|
|
86
|
+
# none: the provenance and immich id survive, the uri moves, shared keys update.
|
|
87
|
+
sha = uuid.uuid4().hex
|
|
88
|
+
first = store.record_asset(
|
|
89
|
+
SourceAsset(uri="quarry://a", sha256=sha, immich_id="im-1", metadata={"license": "cc0", "seen": 1})
|
|
90
|
+
)
|
|
91
|
+
again = store.record_asset(SourceAsset(uri="/data/images/a.png", sha256=sha, metadata={"seen": 2}))
|
|
92
|
+
assert again == first
|
|
93
|
+
with store._acquire() as conn:
|
|
94
|
+
row = conn.execute("SELECT uri, immich_id, metadata FROM source_asset WHERE id = %s", (first,)).fetchone()
|
|
95
|
+
assert row == ("/data/images/a.png", "im-1", {"license": "cc0", "seen": 2})
|
|
96
|
+
|
|
97
|
+
|
|
84
98
|
def test_concurrent_writes_are_isolated(store: PostgresLineageStore) -> None:
|
|
85
99
|
# Many concurrent record_asset calls each borrow their own pooled connection;
|
|
86
100
|
# all succeed with distinct ids (no "another operation in progress").
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|