svitch 0.1.5__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.
- svitch-0.1.5/.gitignore +30 -0
- svitch-0.1.5/PKG-INFO +165 -0
- svitch-0.1.5/README.md +133 -0
- svitch-0.1.5/pyproject.toml +37 -0
- svitch-0.1.5/svitch/__init__.py +14 -0
- svitch-0.1.5/svitch/cache.py +130 -0
- svitch-0.1.5/svitch/classify.py +65 -0
- svitch-0.1.5/svitch/langchain.py +364 -0
- svitch-0.1.5/svitch/otel.py +253 -0
- svitch-0.1.5/svitch/router.py +353 -0
- svitch-0.1.5/svitch/shield.py +307 -0
- svitch-0.1.5/svitch/wrap.py +228 -0
- svitch-0.1.5/svitch_tracer/__init__.py +311 -0
svitch-0.1.5/.gitignore
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.venv/
|
|
8
|
+
venv/
|
|
9
|
+
*.egg
|
|
10
|
+
|
|
11
|
+
# Node
|
|
12
|
+
node_modules/
|
|
13
|
+
sdk/node/dist/
|
|
14
|
+
|
|
15
|
+
# Environment
|
|
16
|
+
.env
|
|
17
|
+
.env.local
|
|
18
|
+
.env.*.local
|
|
19
|
+
|
|
20
|
+
# IDE
|
|
21
|
+
.vscode/
|
|
22
|
+
.idea/
|
|
23
|
+
*.swp
|
|
24
|
+
|
|
25
|
+
# OS
|
|
26
|
+
.DS_Store
|
|
27
|
+
Thumbs.db
|
|
28
|
+
|
|
29
|
+
# Logs
|
|
30
|
+
*.log
|
svitch-0.1.5/PKG-INFO
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: svitch
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: Privacy-first AI infrastructure — PII detection, redaction, agent audit trails, and compliance reports for teams building AI on sensitive data.
|
|
5
|
+
Project-URL: Homepage, https://svitch.ai
|
|
6
|
+
Project-URL: Documentation, https://svitch.ai/dpdp
|
|
7
|
+
Project-URL: Repository, https://github.com/koushiknarendra/svitch
|
|
8
|
+
Project-URL: Issues, https://github.com/koushiknarendra/svitch/issues
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
Keywords: aadhaar,agent,audit,compliance,dpdp,gdpr,hipaa,llm,pan,pii,privacy,security
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Provides-Extra: all
|
|
19
|
+
Requires-Dist: anthropic>=0.25.0; extra == 'all'
|
|
20
|
+
Requires-Dist: langchain-core>=0.2.0; extra == 'all'
|
|
21
|
+
Requires-Dist: openai>=1.0.0; extra == 'all'
|
|
22
|
+
Requires-Dist: opentelemetry-api>=1.0.0; extra == 'all'
|
|
23
|
+
Provides-Extra: anthropic
|
|
24
|
+
Requires-Dist: anthropic>=0.25.0; extra == 'anthropic'
|
|
25
|
+
Provides-Extra: langchain
|
|
26
|
+
Requires-Dist: langchain-core>=0.2.0; extra == 'langchain'
|
|
27
|
+
Provides-Extra: openai
|
|
28
|
+
Requires-Dist: openai>=1.0.0; extra == 'openai'
|
|
29
|
+
Provides-Extra: otel
|
|
30
|
+
Requires-Dist: opentelemetry-api>=1.0.0; extra == 'otel'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# svitch
|
|
34
|
+
|
|
35
|
+
DPDP-compliant AI data security for Indian enterprises.
|
|
36
|
+
Zero dependencies. Runs locally. Works with any LLM provider.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install svitch
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## PII Detection and Redaction
|
|
45
|
+
|
|
46
|
+
Detects and redacts Indian PII (Aadhaar, PAN, UPI, IFSC, mobile, GST, bank accounts) and global PII (email, IP) — entirely locally, no network calls.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import svitch
|
|
50
|
+
|
|
51
|
+
# Detect
|
|
52
|
+
entities = svitch.detect("Customer Aadhaar: 2345 6789 0123, PAN: ABCDE1234F")
|
|
53
|
+
# [Entity(type='AADHAAR', value='2345 6789 0123', ...), Entity(type='PAN', ...)]
|
|
54
|
+
|
|
55
|
+
# Redact (token replacement)
|
|
56
|
+
result = svitch.redact("Call me on 9876543210, UPI: rahul@okicici")
|
|
57
|
+
result.text # "Call me on [MOBILE_IN], UPI: [UPI_ID]"
|
|
58
|
+
result.count # 2
|
|
59
|
+
result.clean # False
|
|
60
|
+
|
|
61
|
+
# Redact (partial mask)
|
|
62
|
+
result = svitch.redact("Aadhaar: 2345 6789 0123", replacement="mask")
|
|
63
|
+
result.text # "Aadhaar: XXXX XXXX 0123"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Wrap any LLM client
|
|
67
|
+
|
|
68
|
+
PII is redacted from every prompt before it leaves your network, and from every response before it's stored or displayed.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import svitch, openai
|
|
72
|
+
|
|
73
|
+
client = svitch.wrap(openai.OpenAI())
|
|
74
|
+
# Use exactly like openai.OpenAI() — PII is handled automatically
|
|
75
|
+
|
|
76
|
+
response = client.chat.completions.create(
|
|
77
|
+
model="gpt-4o",
|
|
78
|
+
messages=[{
|
|
79
|
+
"role": "user",
|
|
80
|
+
"content": "Assess loan for Aadhaar 9876 5432 1098, PAN ABCDE1234F"
|
|
81
|
+
# ↑ redacted to [AADHAAR_IN], [PAN_IN] before reaching OpenAI
|
|
82
|
+
}]
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Anthropic:
|
|
87
|
+
```python
|
|
88
|
+
import svitch, anthropic
|
|
89
|
+
client = svitch.wrap(anthropic.Anthropic())
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Agent Audit Trail
|
|
95
|
+
|
|
96
|
+
Records every agent decision in an immutable, hash-chained audit log.
|
|
97
|
+
Required by DPDP §8 and the RBI FREE AI Framework.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from svitch_tracer import SvitchTracer
|
|
101
|
+
|
|
102
|
+
tracer = SvitchTracer(agent_id="loan-processor-v2")
|
|
103
|
+
|
|
104
|
+
with tracer.run() as run:
|
|
105
|
+
run.data_access(
|
|
106
|
+
source="crm",
|
|
107
|
+
fields_accessed=["name", "income", "aadhaar"],
|
|
108
|
+
purpose="loan_processing",
|
|
109
|
+
data_principal_id="CUST-5821",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
run.llm_call(
|
|
113
|
+
provider="openai",
|
|
114
|
+
model="gpt-4o",
|
|
115
|
+
prompt="Assess eligibility for [AADHAAR_IN] applicant", # already redacted
|
|
116
|
+
response="Eligible. Score: 72/100.",
|
|
117
|
+
redact_pii=True,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
run.decision(
|
|
121
|
+
reason="Score above threshold (70)",
|
|
122
|
+
outcome="approve",
|
|
123
|
+
confidence=0.87,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Human-in-the-loop checkpoint — required for high-risk decisions
|
|
127
|
+
run.human_checkpoint(
|
|
128
|
+
question="Approve ₹5L loan for this applicant?",
|
|
129
|
+
approved=True,
|
|
130
|
+
reviewer_id="anand.k",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Verify the hash chain is intact
|
|
134
|
+
valid, err = run.verify()
|
|
135
|
+
assert valid, f"Audit chain broken: {err}"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Configuration
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
export SVITCH_TRACER_URL=https://agent-tracer.vercel.app # default (hosted)
|
|
142
|
+
# or point to your self-hosted Agent Tracer
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Supported PII types
|
|
148
|
+
|
|
149
|
+
| Type | Pattern |
|
|
150
|
+
|------|---------|
|
|
151
|
+
| `AADHAAR` | 12-digit, masked and unmasked |
|
|
152
|
+
| `PAN` | `ABCDE1234F` format |
|
|
153
|
+
| `UPI_ID` | `handle@provider` |
|
|
154
|
+
| `IFSC` | 11-character bank code |
|
|
155
|
+
| `MOBILE_IN` | 6xxx–9xxx Indian mobile |
|
|
156
|
+
| `BANK_ACCOUNT` | 9–18 digit account numbers |
|
|
157
|
+
| `GST` | `22AAAAA0000A1Z5` |
|
|
158
|
+
| `EMAIL` | RFC 5322 |
|
|
159
|
+
| `IPV4` | IPv4 addresses |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
Apache 2.0 — [svitch.ai](https://svitch.ai) · [DPDP Guide](https://svitch.ai/dpdp) · [GitHub](https://github.com/koushiknarendra/svitch)
|
svitch-0.1.5/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# svitch
|
|
2
|
+
|
|
3
|
+
DPDP-compliant AI data security for Indian enterprises.
|
|
4
|
+
Zero dependencies. Runs locally. Works with any LLM provider.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install svitch
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## PII Detection and Redaction
|
|
13
|
+
|
|
14
|
+
Detects and redacts Indian PII (Aadhaar, PAN, UPI, IFSC, mobile, GST, bank accounts) and global PII (email, IP) — entirely locally, no network calls.
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import svitch
|
|
18
|
+
|
|
19
|
+
# Detect
|
|
20
|
+
entities = svitch.detect("Customer Aadhaar: 2345 6789 0123, PAN: ABCDE1234F")
|
|
21
|
+
# [Entity(type='AADHAAR', value='2345 6789 0123', ...), Entity(type='PAN', ...)]
|
|
22
|
+
|
|
23
|
+
# Redact (token replacement)
|
|
24
|
+
result = svitch.redact("Call me on 9876543210, UPI: rahul@okicici")
|
|
25
|
+
result.text # "Call me on [MOBILE_IN], UPI: [UPI_ID]"
|
|
26
|
+
result.count # 2
|
|
27
|
+
result.clean # False
|
|
28
|
+
|
|
29
|
+
# Redact (partial mask)
|
|
30
|
+
result = svitch.redact("Aadhaar: 2345 6789 0123", replacement="mask")
|
|
31
|
+
result.text # "Aadhaar: XXXX XXXX 0123"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Wrap any LLM client
|
|
35
|
+
|
|
36
|
+
PII is redacted from every prompt before it leaves your network, and from every response before it's stored or displayed.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import svitch, openai
|
|
40
|
+
|
|
41
|
+
client = svitch.wrap(openai.OpenAI())
|
|
42
|
+
# Use exactly like openai.OpenAI() — PII is handled automatically
|
|
43
|
+
|
|
44
|
+
response = client.chat.completions.create(
|
|
45
|
+
model="gpt-4o",
|
|
46
|
+
messages=[{
|
|
47
|
+
"role": "user",
|
|
48
|
+
"content": "Assess loan for Aadhaar 9876 5432 1098, PAN ABCDE1234F"
|
|
49
|
+
# ↑ redacted to [AADHAAR_IN], [PAN_IN] before reaching OpenAI
|
|
50
|
+
}]
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Anthropic:
|
|
55
|
+
```python
|
|
56
|
+
import svitch, anthropic
|
|
57
|
+
client = svitch.wrap(anthropic.Anthropic())
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Agent Audit Trail
|
|
63
|
+
|
|
64
|
+
Records every agent decision in an immutable, hash-chained audit log.
|
|
65
|
+
Required by DPDP §8 and the RBI FREE AI Framework.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from svitch_tracer import SvitchTracer
|
|
69
|
+
|
|
70
|
+
tracer = SvitchTracer(agent_id="loan-processor-v2")
|
|
71
|
+
|
|
72
|
+
with tracer.run() as run:
|
|
73
|
+
run.data_access(
|
|
74
|
+
source="crm",
|
|
75
|
+
fields_accessed=["name", "income", "aadhaar"],
|
|
76
|
+
purpose="loan_processing",
|
|
77
|
+
data_principal_id="CUST-5821",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
run.llm_call(
|
|
81
|
+
provider="openai",
|
|
82
|
+
model="gpt-4o",
|
|
83
|
+
prompt="Assess eligibility for [AADHAAR_IN] applicant", # already redacted
|
|
84
|
+
response="Eligible. Score: 72/100.",
|
|
85
|
+
redact_pii=True,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
run.decision(
|
|
89
|
+
reason="Score above threshold (70)",
|
|
90
|
+
outcome="approve",
|
|
91
|
+
confidence=0.87,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Human-in-the-loop checkpoint — required for high-risk decisions
|
|
95
|
+
run.human_checkpoint(
|
|
96
|
+
question="Approve ₹5L loan for this applicant?",
|
|
97
|
+
approved=True,
|
|
98
|
+
reviewer_id="anand.k",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Verify the hash chain is intact
|
|
102
|
+
valid, err = run.verify()
|
|
103
|
+
assert valid, f"Audit chain broken: {err}"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Configuration
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
export SVITCH_TRACER_URL=https://agent-tracer.vercel.app # default (hosted)
|
|
110
|
+
# or point to your self-hosted Agent Tracer
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Supported PII types
|
|
116
|
+
|
|
117
|
+
| Type | Pattern |
|
|
118
|
+
|------|---------|
|
|
119
|
+
| `AADHAAR` | 12-digit, masked and unmasked |
|
|
120
|
+
| `PAN` | `ABCDE1234F` format |
|
|
121
|
+
| `UPI_ID` | `handle@provider` |
|
|
122
|
+
| `IFSC` | 11-character bank code |
|
|
123
|
+
| `MOBILE_IN` | 6xxx–9xxx Indian mobile |
|
|
124
|
+
| `BANK_ACCOUNT` | 9–18 digit account numbers |
|
|
125
|
+
| `GST` | `22AAAAA0000A1Z5` |
|
|
126
|
+
| `EMAIL` | RFC 5322 |
|
|
127
|
+
| `IPV4` | IPv4 addresses |
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
Apache 2.0 — [svitch.ai](https://svitch.ai) · [DPDP Guide](https://svitch.ai/dpdp) · [GitHub](https://github.com/koushiknarendra/svitch)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "svitch"
|
|
7
|
+
version = "0.1.5"
|
|
8
|
+
description = "Privacy-first AI infrastructure — PII detection, redaction, agent audit trails, and compliance reports for teams building AI on sensitive data."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["pii", "privacy", "llm", "security", "compliance", "gdpr", "dpdp", "hipaa", "aadhaar", "pan", "agent", "audit"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: Apache Software License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Security",
|
|
19
|
+
"Topic :: Software Development :: Libraries",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
openai = ["openai>=1.0.0"]
|
|
25
|
+
anthropic = ["anthropic>=0.25.0"]
|
|
26
|
+
langchain = ["langchain-core>=0.2.0"]
|
|
27
|
+
otel = ["opentelemetry-api>=1.0.0"]
|
|
28
|
+
all = ["openai>=1.0.0", "anthropic>=0.25.0", "langchain-core>=0.2.0", "opentelemetry-api>=1.0.0"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://svitch.ai"
|
|
32
|
+
Documentation = "https://svitch.ai/dpdp"
|
|
33
|
+
Repository = "https://github.com/koushiknarendra/svitch"
|
|
34
|
+
Issues = "https://github.com/koushiknarendra/svitch/issues"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["svitch", "svitch_tracer"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .shield import detect, redact, SvitchResult, Entity
|
|
2
|
+
from .wrap import wrap
|
|
3
|
+
from .router import Router, RouteResult
|
|
4
|
+
from .cache import stabilize, StabilizeResult
|
|
5
|
+
|
|
6
|
+
__all__ = ["detect", "redact", "wrap", "Router", "RouteResult", "SvitchResult", "Entity", "stabilize", "StabilizeResult"]
|
|
7
|
+
__version__ = "0.1.5"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def __getattr__(name: str):
|
|
11
|
+
if name == "SvitchCallbackHandler":
|
|
12
|
+
from .langchain import SvitchCallbackHandler
|
|
13
|
+
return SvitchCallbackHandler
|
|
14
|
+
raise AttributeError(f"module 'svitch' has no attribute {name!r}")
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prefix Stabilizer — normalize system prompts for LLM provider KV cache hits.
|
|
3
|
+
|
|
4
|
+
Unstable system prompts (timestamps, session IDs, request tokens) cause a
|
|
5
|
+
cache miss on every single call, paying full input token cost each time.
|
|
6
|
+
Stabilizing the prefix lets the provider serve from its KV cache:
|
|
7
|
+
|
|
8
|
+
Anthropic — cache-read tokens cost ~10% of normal input token price.
|
|
9
|
+
OpenAI — prompts > 1024 tokens are auto-cached when the prefix is stable.
|
|
10
|
+
|
|
11
|
+
Latency impact: 30-50% lower TTFT on repeated agent calls with the same
|
|
12
|
+
system prompt structure.
|
|
13
|
+
|
|
14
|
+
This is also a compliance signal: timestamps and session IDs embedded in
|
|
15
|
+
system prompts are inadvertent per-request data leaks. `StabilizeResult.removed`
|
|
16
|
+
surfaces exactly what was replaced so the audit trail knows what changed.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import re
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
|
|
24
|
+
# (label, compiled pattern, stable placeholder)
|
|
25
|
+
_UNSTABLE: list[tuple[str, re.Pattern, str]] = [
|
|
26
|
+
(
|
|
27
|
+
"timestamp_iso",
|
|
28
|
+
re.compile(
|
|
29
|
+
r"\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?",
|
|
30
|
+
),
|
|
31
|
+
"[TIMESTAMP]",
|
|
32
|
+
),
|
|
33
|
+
(
|
|
34
|
+
"timestamp_date_phrase",
|
|
35
|
+
re.compile(
|
|
36
|
+
r"(today is|current date[:\s]+|as of[:\s]+)\s*[\w,]+ \d{1,2}[,\s]+\d{4}",
|
|
37
|
+
re.IGNORECASE,
|
|
38
|
+
),
|
|
39
|
+
"[DATE_PHRASE]",
|
|
40
|
+
),
|
|
41
|
+
(
|
|
42
|
+
"unix_timestamp",
|
|
43
|
+
re.compile(r"\b1[6-9]\d{8}\b"),
|
|
44
|
+
"[UNIX_TS]",
|
|
45
|
+
),
|
|
46
|
+
(
|
|
47
|
+
"uuid",
|
|
48
|
+
re.compile(
|
|
49
|
+
r"\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b",
|
|
50
|
+
re.IGNORECASE,
|
|
51
|
+
),
|
|
52
|
+
"[UUID]",
|
|
53
|
+
),
|
|
54
|
+
(
|
|
55
|
+
"session_id",
|
|
56
|
+
re.compile(r"\bsession[_-]?id[:\s]+[\w\-]{8,}\b", re.IGNORECASE),
|
|
57
|
+
"[SESSION_ID]",
|
|
58
|
+
),
|
|
59
|
+
(
|
|
60
|
+
"request_id",
|
|
61
|
+
re.compile(r"\brequest[_-]?id[:\s]+[\w\-]{8,}\b", re.IGNORECASE),
|
|
62
|
+
"[REQUEST_ID]",
|
|
63
|
+
),
|
|
64
|
+
(
|
|
65
|
+
"hex_id",
|
|
66
|
+
re.compile(r"\b[0-9a-f]{24,}\b"),
|
|
67
|
+
"[HEX_ID]",
|
|
68
|
+
),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class StabilizeResult:
|
|
74
|
+
text: str # normalized system prompt — safe to send to the provider
|
|
75
|
+
original: str # unmodified original — preserved for audit
|
|
76
|
+
hash: str # SHA-256 prefix of `text` — use for cache-hit rate tracking
|
|
77
|
+
removed: list[str] # pattern labels replaced (e.g. ["timestamp_iso", "uuid"])
|
|
78
|
+
changed: bool # True when at least one substitution occurred
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def stabilize(system: str) -> StabilizeResult:
|
|
82
|
+
"""
|
|
83
|
+
Normalize a system prompt to maximize LLM provider KV cache hits.
|
|
84
|
+
|
|
85
|
+
Replaces per-request variables (timestamps, UUIDs, session/request IDs)
|
|
86
|
+
with stable placeholders. The original is preserved in `StabilizeResult.original`
|
|
87
|
+
so callers can log it to the audit trail without losing information.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
system: Raw system prompt string.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
StabilizeResult with normalized text, SHA-256 prefix hash, and a list
|
|
94
|
+
of pattern labels that were replaced.
|
|
95
|
+
"""
|
|
96
|
+
if not system:
|
|
97
|
+
return StabilizeResult(text=system, original=system, hash="", removed=[], changed=False)
|
|
98
|
+
|
|
99
|
+
text = system
|
|
100
|
+
removed: list[str] = []
|
|
101
|
+
|
|
102
|
+
for label, pattern, placeholder in _UNSTABLE:
|
|
103
|
+
new_text, n = pattern.subn(placeholder, text)
|
|
104
|
+
if n:
|
|
105
|
+
text = new_text
|
|
106
|
+
removed.append(label)
|
|
107
|
+
|
|
108
|
+
digest = hashlib.sha256(text.encode()).hexdigest()[:16]
|
|
109
|
+
return StabilizeResult(
|
|
110
|
+
text=text,
|
|
111
|
+
original=system,
|
|
112
|
+
hash=digest,
|
|
113
|
+
removed=removed,
|
|
114
|
+
changed=bool(removed),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def anthropic_cache_block(text: str) -> list[dict]:
|
|
119
|
+
"""
|
|
120
|
+
Wrap stabilized text in an Anthropic prompt-caching block.
|
|
121
|
+
|
|
122
|
+
Pass the returned list as the `system` parameter to `client.messages.create()`.
|
|
123
|
+
Anthropic caches tokens at this breakpoint; subsequent requests with the
|
|
124
|
+
same prefix pay ~10% of normal input token cost (minimum 1024 tokens).
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
system_blocks = anthropic_cache_block(stabilize(raw_system).text)
|
|
128
|
+
client.messages.create(model=..., system=system_blocks, messages=...)
|
|
129
|
+
"""
|
|
130
|
+
return [{"type": "text", "text": text, "cache_control": {"type": "ephemeral"}}]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prompt complexity classifier — decides "fast", "default", or "complex".
|
|
3
|
+
Used by the Router to pick the right model tier automatically.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
_COMPLEX_KEYWORDS = frozenset([
|
|
8
|
+
"analyze", "analyse", "explain", "compare", "evaluate", "assess",
|
|
9
|
+
"research", "investigate", "comprehensive", "detailed", "thorough",
|
|
10
|
+
"implement", "architecture", "strategy", "design",
|
|
11
|
+
"write a", "create a", "build a", "develop", "refactor", "debug",
|
|
12
|
+
"step by step", "in depth", "elaborate", "translate",
|
|
13
|
+
"legal", "medical", "financial", "compliance", "audit",
|
|
14
|
+
"summarize", "generate code", "write code", "essay",
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
_SIMPLE_PREFIXES = ("what is ", "who is ", "when did ", "where is ",
|
|
18
|
+
"define ", "list ", "name ")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _token_estimate(text: str) -> int:
|
|
22
|
+
return max(1, int(len(text.split()) * 1.3))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def classify(messages: list[dict]) -> str:
|
|
26
|
+
"""
|
|
27
|
+
Returns "fast", "default", or "complex".
|
|
28
|
+
|
|
29
|
+
fast — simple lookup, extraction, short Q&A → cheapest model
|
|
30
|
+
default — standard reasoning, explanation → standard model
|
|
31
|
+
complex — deep analysis, long-form, code generation → strongest model
|
|
32
|
+
"""
|
|
33
|
+
user_turns = [m for m in messages if m.get("role") == "user"]
|
|
34
|
+
user_text = " ".join(m.get("content", "") for m in user_turns).lower().strip()
|
|
35
|
+
total_tokens = sum(_token_estimate(m.get("content", "")) for m in messages)
|
|
36
|
+
|
|
37
|
+
score = 0
|
|
38
|
+
|
|
39
|
+
# Token volume is the strongest signal
|
|
40
|
+
if total_tokens > 600:
|
|
41
|
+
score += 3
|
|
42
|
+
elif total_tokens > 200:
|
|
43
|
+
score += 1
|
|
44
|
+
|
|
45
|
+
# Multi-turn context
|
|
46
|
+
if len(user_turns) > 2:
|
|
47
|
+
score += 1
|
|
48
|
+
|
|
49
|
+
# Complex keyword hit
|
|
50
|
+
for kw in _COMPLEX_KEYWORDS:
|
|
51
|
+
if kw in user_text:
|
|
52
|
+
score += 2
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
# Simple prefix — negative signal
|
|
56
|
+
for prefix in _SIMPLE_PREFIXES:
|
|
57
|
+
if user_text.startswith(prefix):
|
|
58
|
+
score -= 1
|
|
59
|
+
break
|
|
60
|
+
|
|
61
|
+
if score <= 1:
|
|
62
|
+
return "fast"
|
|
63
|
+
if score <= 4:
|
|
64
|
+
return "default"
|
|
65
|
+
return "complex"
|