aegis-gateway-pack-classification 2.0.0a0__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.
- aegis_gateway_pack_classification-2.0.0a0/.gitignore +100 -0
- aegis_gateway_pack_classification-2.0.0a0/PKG-INFO +10 -0
- aegis_gateway_pack_classification-2.0.0a0/pyproject.toml +26 -0
- aegis_gateway_pack_classification-2.0.0a0/src/aegis_pack_classification/__init__.py +5 -0
- aegis_gateway_pack_classification-2.0.0a0/src/aegis_pack_classification/node.py +67 -0
- aegis_gateway_pack_classification-2.0.0a0/tests/test_classification.py +125 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Node/npm local installs
|
|
2
|
+
node_modules/
|
|
3
|
+
npm-debug.log*
|
|
4
|
+
yarn-debug.log*
|
|
5
|
+
yarn-error.log*
|
|
6
|
+
.pnpm-store/
|
|
7
|
+
|
|
8
|
+
# Local environment files
|
|
9
|
+
.env
|
|
10
|
+
.env.*
|
|
11
|
+
!.env.example
|
|
12
|
+
|
|
13
|
+
## jj
|
|
14
|
+
.jj
|
|
15
|
+
|
|
16
|
+
# Codex local configuration
|
|
17
|
+
.codex/
|
|
18
|
+
AGENTS.md
|
|
19
|
+
|
|
20
|
+
# Claude Flow runtime data
|
|
21
|
+
.claude-flow/data/
|
|
22
|
+
.claude-flow/logs/
|
|
23
|
+
CLAUDE.md
|
|
24
|
+
.claude
|
|
25
|
+
.claude-flow
|
|
26
|
+
|
|
27
|
+
# MCP
|
|
28
|
+
.mcp.json
|
|
29
|
+
ruvector.db
|
|
30
|
+
|
|
31
|
+
# ruflo agents
|
|
32
|
+
.agents
|
|
33
|
+
.swarm
|
|
34
|
+
.agentdb
|
|
35
|
+
agentdb.db
|
|
36
|
+
|
|
37
|
+
# Environment variables
|
|
38
|
+
.env
|
|
39
|
+
.env.local
|
|
40
|
+
.env.*.local
|
|
41
|
+
|
|
42
|
+
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode
|
|
43
|
+
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode
|
|
44
|
+
|
|
45
|
+
### VisualStudioCode ###
|
|
46
|
+
.vscode/*
|
|
47
|
+
!.vscode/settings.json
|
|
48
|
+
!.vscode/tasks.json
|
|
49
|
+
!.vscode/launch.json
|
|
50
|
+
!.vscode/extensions.json
|
|
51
|
+
!.vscode/*.code-snippets
|
|
52
|
+
.vscode
|
|
53
|
+
|
|
54
|
+
# Local History for Visual Studio Code
|
|
55
|
+
.history/
|
|
56
|
+
|
|
57
|
+
# Built Visual Studio Code Extensions
|
|
58
|
+
*.vsix
|
|
59
|
+
|
|
60
|
+
### VisualStudioCode Patch ###
|
|
61
|
+
# Ignore all local history of files
|
|
62
|
+
.history
|
|
63
|
+
.ionide
|
|
64
|
+
|
|
65
|
+
### Misc files
|
|
66
|
+
gemma4
|
|
67
|
+
.postman/
|
|
68
|
+
.openclaude/
|
|
69
|
+
postman/
|
|
70
|
+
|
|
71
|
+
### OpenClaude
|
|
72
|
+
.openclaude-profile.json
|
|
73
|
+
|
|
74
|
+
# Python
|
|
75
|
+
__pycache__/
|
|
76
|
+
*.py[cod]
|
|
77
|
+
*.egg-info/
|
|
78
|
+
.pytest_cache/
|
|
79
|
+
dist/
|
|
80
|
+
build/
|
|
81
|
+
*.egg
|
|
82
|
+
.venv/
|
|
83
|
+
.uv-cache/
|
|
84
|
+
.tmp/
|
|
85
|
+
.ruff_cache/
|
|
86
|
+
|
|
87
|
+
# Docker
|
|
88
|
+
.docker/
|
|
89
|
+
|
|
90
|
+
### Build plans
|
|
91
|
+
BUILD_PLAN.md
|
|
92
|
+
CLAUDE.md
|
|
93
|
+
PROJECT_SPEC.md
|
|
94
|
+
AMEND_PLAN.md
|
|
95
|
+
SOURCE_OF_TRUTH.md
|
|
96
|
+
repo-seed/
|
|
97
|
+
.import_linter_cache/
|
|
98
|
+
site/
|
|
99
|
+
|
|
100
|
+
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aegis-gateway-pack-classification
|
|
3
|
+
Version: 2.0.0a0
|
|
4
|
+
Summary: Aegis v2 — content classification policy pack (regex labeler).
|
|
5
|
+
Project-URL: Homepage, https://github.com/e-choness/aegis
|
|
6
|
+
Project-URL: Repository, https://github.com/e-choness/aegis
|
|
7
|
+
Project-URL: Issues, https://github.com/e-choness/aegis/issues
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Requires-Dist: aegis-gateway-core
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "aegis-gateway-pack-classification"
|
|
3
|
+
version = "2.0.0a0"
|
|
4
|
+
description = "Aegis v2 — content classification policy pack (regex labeler)."
|
|
5
|
+
license = { text = "MIT" }
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
|
|
8
|
+
dependencies = ["aegis-gateway-core"]
|
|
9
|
+
|
|
10
|
+
[project.urls]
|
|
11
|
+
Homepage = "https://github.com/e-choness/aegis"
|
|
12
|
+
Repository = "https://github.com/e-choness/aegis"
|
|
13
|
+
Issues = "https://github.com/e-choness/aegis/issues"
|
|
14
|
+
|
|
15
|
+
[tool.uv.sources]
|
|
16
|
+
aegis-gateway-core = { workspace = true }
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["hatchling"]
|
|
20
|
+
build-backend = "hatchling.build"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/aegis_pack_classification"]
|
|
24
|
+
|
|
25
|
+
[project.entry-points."aegis.nodes"]
|
|
26
|
+
classification = "aegis_pack_classification:ClassificationNode"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""ClassificationNode — regex-based content labeler writing labels.classification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
|
|
8
|
+
from aegis_core.pipeline.state import RunState, RunStateDelta
|
|
9
|
+
|
|
10
|
+
_DEFAULT_PATTERNS: list[tuple[str, str]] = [
|
|
11
|
+
# (label, pattern)
|
|
12
|
+
("pii", r"\b[\w.+-]+@[\w-]+\.[a-z]{2,}\b"), # email
|
|
13
|
+
("pii", r"\b\d{3}[-.\s]\d{3}[-.\s]\d{4}\b"), # US phone
|
|
14
|
+
("financial", r"\b\d{4}[\s-]\d{4}[\s-]\d{4}[\s-]\d{4}\b"), # credit card
|
|
15
|
+
("secret", r"(?i)\b(api[_-]?key|password|secret|token)\s*[=:]\s*\S+"),
|
|
16
|
+
("medical", r"(?i)\b(diagnosis|prescription|patient|hipaa)\b"),
|
|
17
|
+
("legal", r"(?i)\b(attorney.client|privileged|confidential)\b"),
|
|
18
|
+
("public", r".*"), # catch-all fallback
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ClassificationNode:
|
|
23
|
+
"""Pipeline node that classifies the last user message via regex rules.
|
|
24
|
+
|
|
25
|
+
Writes the matched label into ``state.labels["classification"]``.
|
|
26
|
+
If no pattern matches (impossible with the default catch-all, but possible
|
|
27
|
+
with a custom ``patterns`` list), the label is left unchanged.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
patterns: Ordered list of ``(label, regex_pattern)`` pairs. First
|
|
31
|
+
match wins. Defaults to a built-in set covering PII, financial,
|
|
32
|
+
secrets, medical, legal, and public content.
|
|
33
|
+
name: Node name.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
patterns: Sequence[tuple[str, str]] = _DEFAULT_PATTERNS,
|
|
39
|
+
name: str = "classification",
|
|
40
|
+
) -> None:
|
|
41
|
+
self.name = name
|
|
42
|
+
self._rules: list[tuple[str, re.Pattern[str]]] = [
|
|
43
|
+
(label, re.compile(pattern)) for label, pattern in patterns
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
async def run(self, state: RunState) -> RunStateDelta:
|
|
47
|
+
"""Classify the last user message and return a labels delta."""
|
|
48
|
+
text: str | None = None
|
|
49
|
+
for msg in reversed(state.messages):
|
|
50
|
+
if msg.role == "user":
|
|
51
|
+
text = msg.content
|
|
52
|
+
break
|
|
53
|
+
|
|
54
|
+
if text is None:
|
|
55
|
+
return RunStateDelta()
|
|
56
|
+
|
|
57
|
+
label = self._classify(text)
|
|
58
|
+
if label is None:
|
|
59
|
+
return RunStateDelta()
|
|
60
|
+
|
|
61
|
+
return RunStateDelta(labels={"classification": label})
|
|
62
|
+
|
|
63
|
+
def _classify(self, text: str) -> str | None:
|
|
64
|
+
for label, pattern in self._rules:
|
|
65
|
+
if pattern.search(text):
|
|
66
|
+
return label
|
|
67
|
+
return None
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Tests for aegis-pack-classification.
|
|
2
|
+
|
|
3
|
+
Gate: DC uv run pytest packages/aegis-pack-classification -q
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from aegis_pack_classification import ClassificationNode
|
|
9
|
+
|
|
10
|
+
from aegis_core.pipeline.state import RunState
|
|
11
|
+
from aegis_core.providers.models import Message
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# Helpers
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _state(*contents: str, role: str = "user") -> RunState:
|
|
19
|
+
return RunState(
|
|
20
|
+
run_id="test",
|
|
21
|
+
route="default",
|
|
22
|
+
messages=[Message(role=role, content=c) for c in contents],
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# ClassificationNode — basic labeling
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestClassificationNode:
|
|
32
|
+
async def test_classifies_email_as_pii(self) -> None:
|
|
33
|
+
node = ClassificationNode()
|
|
34
|
+
delta = await node.run(_state("My email is alice@example.com"))
|
|
35
|
+
assert delta.labels is not None
|
|
36
|
+
assert delta.labels["classification"] == "pii"
|
|
37
|
+
|
|
38
|
+
async def test_classifies_phone_as_pii(self) -> None:
|
|
39
|
+
node = ClassificationNode()
|
|
40
|
+
delta = await node.run(_state("Call me at 555-867-5309"))
|
|
41
|
+
assert delta.labels is not None
|
|
42
|
+
assert delta.labels["classification"] == "pii"
|
|
43
|
+
|
|
44
|
+
async def test_classifies_credit_card_as_financial(self) -> None:
|
|
45
|
+
node = ClassificationNode()
|
|
46
|
+
delta = await node.run(_state("Card: 4111 1111 1111 1111"))
|
|
47
|
+
assert delta.labels is not None
|
|
48
|
+
assert delta.labels["classification"] == "financial"
|
|
49
|
+
|
|
50
|
+
async def test_classifies_api_key_as_secret(self) -> None:
|
|
51
|
+
node = ClassificationNode()
|
|
52
|
+
delta = await node.run(_state("api_key=sk-abc123"))
|
|
53
|
+
assert delta.labels is not None
|
|
54
|
+
assert delta.labels["classification"] == "secret"
|
|
55
|
+
|
|
56
|
+
async def test_classifies_password_as_secret(self) -> None:
|
|
57
|
+
node = ClassificationNode()
|
|
58
|
+
delta = await node.run(_state("password: hunter2"))
|
|
59
|
+
assert delta.labels is not None
|
|
60
|
+
assert delta.labels["classification"] == "secret"
|
|
61
|
+
|
|
62
|
+
async def test_classifies_medical(self) -> None:
|
|
63
|
+
node = ClassificationNode()
|
|
64
|
+
delta = await node.run(_state("Patient diagnosis: hypertension"))
|
|
65
|
+
assert delta.labels is not None
|
|
66
|
+
assert delta.labels["classification"] == "medical"
|
|
67
|
+
|
|
68
|
+
async def test_classifies_legal(self) -> None:
|
|
69
|
+
node = ClassificationNode()
|
|
70
|
+
delta = await node.run(_state("This is attorney-client privileged"))
|
|
71
|
+
assert delta.labels is not None
|
|
72
|
+
assert delta.labels["classification"] == "legal"
|
|
73
|
+
|
|
74
|
+
async def test_classifies_plain_text_as_public(self) -> None:
|
|
75
|
+
node = ClassificationNode()
|
|
76
|
+
delta = await node.run(_state("What is the weather today?"))
|
|
77
|
+
assert delta.labels is not None
|
|
78
|
+
assert delta.labels["classification"] == "public"
|
|
79
|
+
|
|
80
|
+
async def test_uses_last_user_message(self) -> None:
|
|
81
|
+
node = ClassificationNode()
|
|
82
|
+
state = RunState(
|
|
83
|
+
run_id="r",
|
|
84
|
+
route="d",
|
|
85
|
+
messages=[
|
|
86
|
+
Message(role="user", content="What is the weather?"),
|
|
87
|
+
Message(role="assistant", content="It is sunny."),
|
|
88
|
+
Message(role="user", content="My email is bob@test.com"),
|
|
89
|
+
],
|
|
90
|
+
)
|
|
91
|
+
delta = await node.run(state)
|
|
92
|
+
assert delta.labels is not None
|
|
93
|
+
assert delta.labels["classification"] == "pii"
|
|
94
|
+
|
|
95
|
+
async def test_no_user_message_returns_empty_delta(self) -> None:
|
|
96
|
+
node = ClassificationNode()
|
|
97
|
+
state = RunState(
|
|
98
|
+
run_id="r",
|
|
99
|
+
route="d",
|
|
100
|
+
messages=[Message(role="assistant", content="hello")],
|
|
101
|
+
)
|
|
102
|
+
delta = await node.run(state)
|
|
103
|
+
assert delta.labels is None
|
|
104
|
+
|
|
105
|
+
async def test_custom_patterns_first_match_wins(self) -> None:
|
|
106
|
+
node = ClassificationNode(patterns=[
|
|
107
|
+
("top_secret", r"classified"),
|
|
108
|
+
("public", r".*"),
|
|
109
|
+
])
|
|
110
|
+
delta = await node.run(_state("This document is classified"))
|
|
111
|
+
assert delta.labels is not None
|
|
112
|
+
assert delta.labels["classification"] == "top_secret"
|
|
113
|
+
|
|
114
|
+
async def test_no_match_with_empty_patterns(self) -> None:
|
|
115
|
+
node = ClassificationNode(patterns=[])
|
|
116
|
+
delta = await node.run(_state("anything"))
|
|
117
|
+
assert delta.labels is None
|
|
118
|
+
|
|
119
|
+
def test_node_name_default(self) -> None:
|
|
120
|
+
node = ClassificationNode()
|
|
121
|
+
assert node.name == "classification"
|
|
122
|
+
|
|
123
|
+
def test_node_name_custom(self) -> None:
|
|
124
|
+
node = ClassificationNode(name="my_classifier")
|
|
125
|
+
assert node.name == "my_classifier"
|