argus-cortex 0.4.0__tar.gz → 0.4.1__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 (43) hide show
  1. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/CHANGELOG.md +15 -1
  2. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/PKG-INFO +4 -1
  3. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/README.md +3 -0
  4. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/_version.py +2 -2
  5. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/moderation/singguard.py +20 -3
  6. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/tests/test_moderation.py +20 -3
  7. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/.copier-answers.yml +0 -0
  8. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/.env.example +0 -0
  9. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/.github/workflows/ci.yml +0 -0
  10. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/.github/workflows/release.yml +0 -0
  11. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/.gitignore +0 -0
  12. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/CLAUDE.md +0 -0
  13. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/LICENSE +0 -0
  14. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/Makefile +0 -0
  15. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/pyproject.toml +0 -0
  16. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/__init__.py +0 -0
  17. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/backends.py +0 -0
  18. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/moderation/__init__.py +0 -0
  19. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/moderation/_http.py +0 -0
  20. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/moderation/nudenet.py +0 -0
  21. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/py.typed +0 -0
  22. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/server.py +0 -0
  23. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/store/__init__.py +0 -0
  24. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/store/blob.py +0 -0
  25. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/store/config.py +0 -0
  26. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/store/errors.py +0 -0
  27. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/store/lineage.py +0 -0
  28. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/store/models.py +0 -0
  29. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/store/schema.py +0 -0
  30. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/store/vector.py +0 -0
  31. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/taxonomy.py +0 -0
  32. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/wire.py +0 -0
  33. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/src/argus_cortex/xmp.py +0 -0
  34. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/tests/test_backends.py +0 -0
  35. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/tests/test_blob.py +0 -0
  36. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/tests/test_server.py +0 -0
  37. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/tests/test_smoke.py +0 -0
  38. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/tests/test_store.py +0 -0
  39. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/tests/test_store_pg_integration.py +0 -0
  40. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/tests/test_taxonomy.py +0 -0
  41. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/tests/test_vector.py +0 -0
  42. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/tests/test_wire.py +0 -0
  43. {argus_cortex-0.4.0 → argus_cortex-0.4.1}/tests/test_xmp.py +0 -0
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.1] - 2026-10-27
11
+
12
+ ### Added
13
+ - `SingGuardClient(thinking="slow")` and `thinking="cascade"`: the argus-cull
14
+ `singguard` service's modes. `slow` reasons with no fast first-line verdict,
15
+ so the reasoning is not anchored to a verdict made before it; `cascade` runs
16
+ `fast` on every image and `slow` only on the ones fast flags, and slow's
17
+ verdict stands. `THINKING_MODES` lists them. A SingGuard server without a
18
+ mode answers HTTP 400, raised as `GuardCheckFailed` per check.
19
+ - `GuardResult.thinking`: the mode the server says it judged in (`None` from a
20
+ server that does not say). The argus-cull service can be forced server-side
21
+ to a mode other than the one asked for, and this records which it used.
22
+
10
23
  ## [0.4.0] - 2026-10-26
11
24
 
12
25
  ### Added
@@ -103,7 +116,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
103
116
  (`LocalBackend`, `RemoteBackend`, `RemoteProvider`, `with_retries`
104
117
  retrying only transient errors, `resolve_device`).
105
118
 
106
- [Unreleased]: https://github.com/smk762/argus-cortex/compare/v0.4.0...HEAD
119
+ [Unreleased]: https://github.com/smk762/argus-cortex/compare/v0.4.1...HEAD
120
+ [0.4.1]: https://github.com/smk762/argus-cortex/compare/v0.4.0...v0.4.1
107
121
  [0.4.0]: https://github.com/smk762/argus-cortex/compare/v0.3.0...v0.4.0
108
122
  [0.3.0]: https://github.com/smk762/argus-cortex/compare/v0.2.0...v0.3.0
109
123
  [0.2.0]: https://github.com/smk762/argus-cortex/compare/v0.1.0...v0.2.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: argus-cortex
3
- Version: 0.4.0
3
+ Version: 0.4.1
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
@@ -99,6 +99,9 @@ result = nudenet.check(image_bytes, "beach.jpg")
99
99
  result.exposed_score # 0.0-1.0 over DEFAULT_EXPOSED_LABELS; result.labels = per-class maxima
100
100
 
101
101
  guard = SingGuardClient("http://guard:9150", policy=None) # None = SingGuard's default rules
102
+ # thinking="fast" (default) is a verdict and rule; "fast-slow" also writes why; "slow" writes why
103
+ # with no fast verdict ahead of the reasoning; "cascade" is fast, then slow only for what fast
104
+ # flagged (the last two: argus-cull's singguard service only).
102
105
  verdict = guard.check(image_bytes, text="the prompt, if any")
103
106
  verdict.safe, verdict.rules # False, ["B. Real-World Crimes & Public Safety"]
104
107
  any(rule_matches(r, "B") for r in verdict.rules) # match a rule by letter or full name
@@ -53,6 +53,9 @@ result = nudenet.check(image_bytes, "beach.jpg")
53
53
  result.exposed_score # 0.0-1.0 over DEFAULT_EXPOSED_LABELS; result.labels = per-class maxima
54
54
 
55
55
  guard = SingGuardClient("http://guard:9150", policy=None) # None = SingGuard's default rules
56
+ # thinking="fast" (default) is a verdict and rule; "fast-slow" also writes why; "slow" writes why
57
+ # with no fast verdict ahead of the reasoning; "cascade" is fast, then slow only for what fast
58
+ # flagged (the last two: argus-cull's singguard service only).
56
59
  verdict = guard.check(image_bytes, text="the prompt, if any")
57
60
  verdict.safe, verdict.rules # False, ["B. Real-World Crimes & Public Safety"]
58
61
  any(rule_matches(r, "B") for r in verdict.rules) # match a rule by letter or full name
@@ -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.0'
22
- __version_tuple__ = version_tuple = (0, 4, 0)
21
+ __version__ = version = '0.4.1'
22
+ __version_tuple__ = version_tuple = (0, 4, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -9,7 +9,17 @@ Animal Abuse. A custom policy text replaces them. It gives no scores.
9
9
 
10
10
  API: ``POST /api/guard/check`` with JSON ``{input, image_base64, policy,
11
11
  thinking}``, answered by ``{safe, verdict, rule, explanation, categories:
12
- [{code, label, why}], model, elapsed_ms}``.
12
+ [{code, label, why}], model, elapsed_ms}`` and, from the argus-cull
13
+ ``singguard`` service, ``thinking``: the mode the server judged in, which
14
+ its ``SINGGUARD_FORCE_THINKING`` can make differ from the one asked for.
15
+
16
+ ``thinking`` is ``fast`` (a verdict and rule), ``fast-slow`` (SingGuard's own
17
+ reasoning mode: a fast first-line verdict, then the reasoning and a final
18
+ answer), ``slow`` (the argus-cull service's variant with no first-line
19
+ verdict, so the reasoning is not anchored to a verdict made before it) or
20
+ ``cascade`` (that service's ``fast`` for every image and ``slow`` for the ones
21
+ fast flags, slow's verdict standing). Servers without ``slow`` and ``cascade``
22
+ answer HTTP 400 to them.
13
23
 
14
24
  Large images are downscaled before sending (Pillow, from the ``[images]``
15
25
  extra, imported only when downscaling is on).
@@ -27,7 +37,7 @@ from pydantic import BaseModel, Field
27
37
 
28
38
  from argus_cortex.moderation._http import CheckFailed, ModerationConfigError, post_json, service_url
29
39
 
30
- THINKING_MODES = ("fast", "fast-slow")
40
+ THINKING_MODES = ("fast", "fast-slow", "slow", "cascade")
31
41
  # The guard model sees far fewer pixels than a camera original, and base64
32
42
  # JSON inflates every byte by 4/3.
33
43
  MAX_EDGE = 1536
@@ -51,6 +61,9 @@ class GuardResult(BaseModel):
51
61
  # own rules) or "custom-<sha256 prefix>" of the policy text.
52
62
  model: str | None = None
53
63
  policy: str = "default"
64
+ # The thinking mode the server judged in, when it says (the argus-cull
65
+ # service does; it can be forced server-side to differ from the request).
66
+ thinking: str | None = None
54
67
 
55
68
 
56
69
  class GuardCheckFailed(CheckFailed):
@@ -104,6 +117,7 @@ def to_result(payload: object, policy: str = "default") -> GuardResult:
104
117
  explanation=_clip(payload.get("explanation"), _EXPLANATION_MAX),
105
118
  model=_clip(payload.get("model"), _FIELD_MAX),
106
119
  policy=policy,
120
+ thinking=_clip(payload.get("thinking"), _FIELD_MAX),
107
121
  )
108
122
 
109
123
 
@@ -154,7 +168,10 @@ class SingGuardClient:
154
168
  """A SingGuard server at *base_url* (its origin, e.g. ``http://guard:9150``).
155
169
 
156
170
  *policy* is the policy text (``None`` = SingGuard's default rules);
157
- *thinking* is ``fast`` (verdict and rule) or ``fast-slow`` (also writes why).
171
+ *thinking* is one of :data:`THINKING_MODES`: ``fast`` (verdict and rule),
172
+ ``fast-slow`` (also writes why), ``slow`` (writes why, with no fast
173
+ verdict ahead of the reasoning) or ``cascade`` (fast, then slow only for
174
+ what fast flagged); the last two need the argus-cull ``singguard`` service.
158
175
  With *downscale* on (the default, needs ``[images]``), images over
159
176
  :data:`MAX_EDGE` px or :data:`MAX_BYTES` are shrunk before sending.
160
177
  Thread-safe: :meth:`check` holds no state.
@@ -102,7 +102,7 @@ class _NudeNet(_Fake):
102
102
 
103
103
 
104
104
  class _SingGuard(_Fake):
105
- answers: dict[int, dict] = {}
105
+ answers: dict[int, dict] = {} # a test's overrides of _GUARD_ANSWERS, by red value
106
106
 
107
107
  def do_POST(self) -> None:
108
108
  body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
@@ -115,7 +115,8 @@ class _SingGuard(_Fake):
115
115
  if red == _FAIL:
116
116
  self._send(500)
117
117
  else:
118
- self._send(200, {**_GUARD_ANSWERS.get(red, _GUARD_ANSWERS[10]), "model": "SingGuard-4b", "elapsed_ms": 5})
118
+ answer = self.answers.get(red) or _GUARD_ANSWERS.get(red, _GUARD_ANSWERS[10])
119
+ self._send(200, {**answer, "model": "SingGuard-4b", "elapsed_ms": 5})
119
120
 
120
121
 
121
122
  def _serve(handler: type[_Fake]) -> Iterator[tuple[type[_Fake], str]]:
@@ -239,7 +240,8 @@ def test_singguard_config() -> None:
239
240
  assert custom.policy == "### A. Minors" and custom.policy_name == policy_name("### A. Minors")
240
241
  assert custom.policy_name == policy_name(" ### A. Minors\n") # a policy read from a file matches
241
242
  assert custom.policy_name.startswith("custom-") and len(custom.policy_name) == len("custom-") + 12
242
- for kwargs in ({"thinking": "slow"}, {"policy": " \n"}):
243
+ assert [SingGuardClient("http://g", thinking=m).thinking for m in ("slow", "cascade")] == ["slow", "cascade"]
244
+ for kwargs in ({"thinking": "slower"}, {"thinking": "FAST"}, {"policy": " \n"}):
243
245
  with pytest.raises(ModerationConfigError):
244
246
  SingGuardClient("http://g", **kwargs)
245
247
  with pytest.raises(ModerationConfigError):
@@ -269,6 +271,21 @@ def test_singguard_default_policy_is_not_sent(singguard_server) -> None:
269
271
  assert "policy" not in fake.seen[0]
270
272
 
271
273
 
274
+ def test_singguard_slow_is_sent_and_the_mode_used_is_recorded(singguard_server) -> None:
275
+ """The argus-cull service answers with the mode it judged in, which a
276
+ server-side force can make differ from the request; it is recorded as sent.
277
+ A server that says nothing leaves it None."""
278
+ fake, url = singguard_server
279
+ client = SingGuardClient(url, timeout=5, thinking="slow")
280
+ assert client.check(_png(10)).thinking is None
281
+ fake.answers = {10: {**_GUARD_ANSWERS[10], "thinking": "fast-slow"}}
282
+ try:
283
+ assert client.check(_png(10)).thinking == "fast-slow"
284
+ finally:
285
+ fake.answers = {}
286
+ assert {s["thinking"] for s in fake.seen} == {"slow"}
287
+
288
+
272
289
  def test_singguard_downscales_large_images(singguard_server) -> None:
273
290
  fake, url = singguard_server
274
291
  SingGuardClient(url).check(_png(10, (4000, 3000)))
File without changes
File without changes
File without changes
File without changes
File without changes