pakka 0.1.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 (50) hide show
  1. pakka-0.1.0/LICENSE +21 -0
  2. pakka-0.1.0/PKG-INFO +290 -0
  3. pakka-0.1.0/README.md +259 -0
  4. pakka-0.1.0/pakka/__init__.py +1 -0
  5. pakka-0.1.0/pakka/approve.py +44 -0
  6. pakka-0.1.0/pakka/bench.py +380 -0
  7. pakka-0.1.0/pakka/cache.py +83 -0
  8. pakka-0.1.0/pakka/cli.py +20 -0
  9. pakka-0.1.0/pakka/embed.py +104 -0
  10. pakka-0.1.0/pakka/flow.py +83 -0
  11. pakka-0.1.0/pakka/guard.py +238 -0
  12. pakka-0.1.0/pakka/pii.py +81 -0
  13. pakka-0.1.0/pakka/policy.py +45 -0
  14. pakka-0.1.0/pakka/redis_cache.py +62 -0
  15. pakka-0.1.0/pakka/review.py +74 -0
  16. pakka-0.1.0/pakka/testset.py +158 -0
  17. pakka-0.1.0/pakka/train.py +47 -0
  18. pakka-0.1.0/pakka/verify.py +128 -0
  19. pakka-0.1.0/pakka.egg-info/PKG-INFO +290 -0
  20. pakka-0.1.0/pakka.egg-info/SOURCES.txt +48 -0
  21. pakka-0.1.0/pakka.egg-info/dependency_links.txt +1 -0
  22. pakka-0.1.0/pakka.egg-info/entry_points.txt +2 -0
  23. pakka-0.1.0/pakka.egg-info/requires.txt +19 -0
  24. pakka-0.1.0/pakka.egg-info/top_level.txt +1 -0
  25. pakka-0.1.0/pyproject.toml +43 -0
  26. pakka-0.1.0/setup.cfg +4 -0
  27. pakka-0.1.0/tests/test_approve.py +18 -0
  28. pakka-0.1.0/tests/test_bench.py +329 -0
  29. pakka-0.1.0/tests/test_bot.py +148 -0
  30. pakka-0.1.0/tests/test_cache.py +65 -0
  31. pakka-0.1.0/tests/test_cli.py +19 -0
  32. pakka-0.1.0/tests/test_data.py +29 -0
  33. pakka-0.1.0/tests/test_e2e.py +23 -0
  34. pakka-0.1.0/tests/test_e2e_verify.py +20 -0
  35. pakka-0.1.0/tests/test_fetch_public.py +210 -0
  36. pakka-0.1.0/tests/test_flow.py +129 -0
  37. pakka-0.1.0/tests/test_guard.py +226 -0
  38. pakka-0.1.0/tests/test_ingest_human.py +164 -0
  39. pakka-0.1.0/tests/test_integration.py +102 -0
  40. pakka-0.1.0/tests/test_label_audit.py +68 -0
  41. pakka-0.1.0/tests/test_llm.py +46 -0
  42. pakka-0.1.0/tests/test_phase10.py +45 -0
  43. pakka-0.1.0/tests/test_phase9.py +28 -0
  44. pakka-0.1.0/tests/test_pii.py +53 -0
  45. pakka-0.1.0/tests/test_policy.py +79 -0
  46. pakka-0.1.0/tests/test_replay.py +32 -0
  47. pakka-0.1.0/tests/test_review.py +23 -0
  48. pakka-0.1.0/tests/test_testset.py +83 -0
  49. pakka-0.1.0/tests/test_train.py +20 -0
  50. pakka-0.1.0/tests/test_verify.py +121 -0
pakka-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mohammedp010
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pakka-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,290 @@
1
+ Metadata-Version: 2.4
2
+ Name: pakka
3
+ Version: 0.1.0
4
+ Summary: Tests, guards and safety rules for semantic caches, with English and Hinglish support
5
+ Author: mohammedp010
6
+ License-Expression: MIT
7
+ Keywords: semantic cache,llm,evaluation,hinglish,rag,guardrails,pii
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
11
+ Classifier: Natural Language :: English
12
+ Classifier: Natural Language :: Hindi
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: numpy
17
+ Requires-Dist: sentence-transformers>=3
18
+ Provides-Extra: redis
19
+ Requires-Dist: redisvl>=0.4; extra == "redis"
20
+ Provides-Extra: bot
21
+ Requires-Dist: fastapi; extra == "bot"
22
+ Requires-Dist: uvicorn; extra == "bot"
23
+ Requires-Dist: httpx; extra == "bot"
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest; extra == "dev"
26
+ Provides-Extra: verify
27
+ Requires-Dist: httpx; extra == "verify"
28
+ Provides-Extra: train
29
+ Requires-Dist: sentence-transformers[train]>=3; extra == "train"
30
+ Dynamic: license-file
31
+
32
+ # pakka
33
+
34
+ **Measure and cut the wrong answers a semantic cache serves, in English and Hinglish support bots.**
35
+
36
+ A semantic cache answers a repeat question from memory when its meaning matches an earlier one. Redis, Azure,
37
+ Kong, LiteLLM and most gateways ship one. None of them tells you **how often the cache serves a confident,
38
+ wrong answer.** Pakka measures that, blocks the common causes, and keeps the cache from leaking or going stale.
39
+ It is a Python library plus a benchmark, tested on nine public datasets other people built and labelled, plus
40
+ 315 messages real people wrote.
41
+
42
+ *Pakka* is Hinglish for "sure": *pakka sure hai?*
43
+
44
+ ## Results
45
+
46
+ ![RedisVL's default cache on ten datasets: more than 5% of its hits are wrong on six of them, up to 66%](docs/img/redisvl_defaults.png)
47
+
48
+ - **Defaults are not safe.** RedisVL's out-of-the-box cache (`langcache-embed-v1` at similarity 0.90) serves more
49
+ than 5% wrong answers on 6 of 10 datasets, from 7% (vCache LmArena) to 66% (Hinglish-TOP). No single threshold
50
+ carries over between datasets or embedders.
51
+ - **In a real bot, a cache saves money but is wrong far more often than the LLM.** Dukaan Bot on 4,000 held-out
52
+ Bitext questions, every served reply checked by three LLM judges: the cache answers 10–52% of questions and
53
+ cuts LLM spend by 10–51%, but 15–27% of the cached answers are wrong, against 3–4% for the LLM's own answers
54
+ to the same questions (6–9 times as often).
55
+ - **Real Hinglish is much harder than LLM-written Hinglish.** On 258 messages people wrote, the free default
56
+ answers 3 (1 wrong). v1's LLM-written test made pakka look far better than this; see
57
+ [what v1 got wrong](docs/report.md#limitations).
58
+
59
+ **The real-bot run** (Phase 10): 4,000 Bitext test questions, Bitext's 27 e-commerce answers as the knowledge
60
+ base, `claude-haiku-5.5` as the LLM. Wrong = judged wrong by the LLM judges, not by intent labels.
61
+
62
+ | Setup | Answered from cache | Wrong answers served (95% upper) | Hit time p50 | LLM spend saved, verifier paid |
63
+ |---|---|---|---|---|
64
+ | No cache | 0 | LLM's own: 3–4% | 1,248 ms | - |
65
+ | **MiniLM-L12 (default), free** | 9.8% | 103 of 394: 26% (31%) | 9 ms | 9.9% |
66
+ | Fine-tuned MiniLM + cheap LLM verifier | 52.3% | 554 of 2,091: 26.5% (28%) | 777 ms | 51.3% |
67
+ | Fine-tuned MiniLM + Jev verifier | 30.9% | 187 of 1,236: 15% (17%) | 326 ms | 27.8% |
68
+ | Qwen3-0.6B (GPU) + Jev verifier | 38.1% | 254 of 1,525: 17% (19%) | 379 ms | 35.1% |
69
+
70
+ ![Share answered from cache against share of cached answers judged wrong, for the four setups; the LLM's own wrong rate is 3-4%](docs/img/bot_run.png)
71
+
72
+ Most of those wrong answers come from a few FAQ answers that only fit their own seed question: Bitext's "cancel
73
+ order" answer just says "please ask your question" (27 of the free setup's 103, 61 of Jev's 187). **Seed a cache
74
+ with answers that stand on their own.**
75
+
76
+ **Our own sets**, guards on, threshold picked on v1's dev split. Cells are right / wrong answers served from cache
77
+ (95% upper bound on the false-hit rate). v1 test is 526 LLM-written questions; human test is 258 messages real
78
+ people wrote (80% romanized Hinglish), the harder and more honest one.
79
+
80
+ | Setup | v1 test (526) | Human test (258) | Hinglish caught (human) |
81
+ |---|---|---|---|
82
+ | **Free:** MiniLM-L12 (default), 0.79, 10% budget | 47 / 5 (20.6%) | 2 / 1 (79.2%) | 0/184 |
83
+ | **Free:** Qwen3-Embedding-0.6B (GPU option), 0.925, 10% budget | 102 / 8 (13.7%) | 11 / 0 (25.9%) | 6/184 |
84
+ | MiniLM-L12 + cheap LLM verifier | 118 / 6 (10.2%) | 23 / 0 (14.3%) | 12/184 |
85
+ | MiniLM-L12 + Jev verifier | 96 / 1 (5.6%) | 19 / 0 (16.8%) | 10/184 |
86
+ | Fine-tuned MiniLM-L12 (opt-in) + Jev | 148 / 3 (5.7%) | 54 / 1 (9.6%) | 44/184 |
87
+ | Qwen3-0.6B + cheap LLM | 231 / 13 (8.9%) | 113 / 9 (13.4%) | 97/184 |
88
+ | Qwen3-0.6B + Jev | 181 / 3 (4.7%) | 80 / 1 (6.7%) | 67/184 |
89
+
90
+ Without a verifier no threshold fits a 5% budget for either embedder. Everything: [the tech report](docs/report.md),
91
+ [public results](docs/public_results.md) (every table, with a one-command reproduce) and the
92
+ [lab notebook](docs/results.md).
93
+
94
+ ## When pakka helps, and when it doesn't
95
+
96
+ **It helps when:**
97
+ - you run a **high-volume FAQ or support bot** where many people ask the same few things;
98
+ - your users write **Hinglish or Hindi**: the guards read *nahi*, *kal*, *band karo*, Devanagari digits and the like,
99
+ and the benchmark tells you how your embedder does on real Hinglish;
100
+ - you are **cost-sensitive**: free hits take 9 ms on a CPU instead of a 1.2 s LLM call;
101
+ - you are **multi-tenant with PII**: per-user partitions, a PII gate that never stores Aadhaar, PAN, UPI, phone or
102
+ order IDs, and an approval queue for shared answers.
103
+
104
+ **It doesn't help, or isn't worth it, when:**
105
+ - prompts are **unique or long** (chat, coding, agents): few repeats, and long texts all look alike to an embedder;
106
+ - answers are **personal** ("where is my order?"): the answer depends on the user, not the question;
107
+ - answers **change fast** (stock, prices, live status): you'll spend your time invalidating;
108
+ - traffic is **low**: there is little to reuse, and you can't measure the cache on your own traffic;
109
+ - the domain has **zero tolerance** for a wrong answer (medical, legal, money), unless a human approves every
110
+ answer the cache may serve. Even the best setup above is wrong on 15% of its hits on Bitext.
111
+
112
+ ## Install
113
+
114
+ ```bash
115
+ pip install "pakka @ git+https://github.com/mohammedp010/pakka" # default: free, offline, CPU
116
+ pip install "pakka[verify] @ git+https://github.com/mohammedp010/pakka" # + the optional verifier's HTTP client
117
+ ```
118
+
119
+ Not on PyPI yet. From a clone: `pip install -e ".[verify]"`.
120
+
121
+ ## Quickstart
122
+
123
+ **Default: free, offline, MiniLM-L12 on a CPU.** Guard a cache you already have:
124
+
125
+ ```python
126
+ from pakka.guard import check
127
+
128
+ check("refund nahi mila", "How long does a refund take?") # "negation"
129
+ check("Is Cash on Delivery available in Nepal?", "Is Cash on Delivery available?") # "entity"
130
+ check("COD available hai kya?", "Is Cash on Delivery available?") # None: safe to serve
131
+ ```
132
+
133
+ Or use the full flow:
134
+
135
+ ```python
136
+ from pakka.cache import NumpyCache # or pakka.redis_cache.RedisVLCache
137
+ from pakka.embed import load_embedder
138
+ from pakka.flow import CachedAnswerer
139
+ from pakka.guard import check
140
+ from pakka.policy import GLOBAL, Policy
141
+
142
+ def my_llm(question): # your LLM call: returns (answer, tokens used)
143
+ ...
144
+
145
+ cache = NumpyCache(load_embedder()) # MiniLM-L12
146
+ cache.store("How long does a refund take?", "Refunds take 5-7 business days.",
147
+ {"id": "faq_01", "partition": GLOBAL, "kb_version": "v1"})
148
+
149
+ bot = CachedAnswerer(cache, my_llm, threshold=0.79, guard=check, policy=Policy(kb_version="v1"))
150
+ r = bot.ask("How many days does a refund take?", partition="user-42")
151
+ r.hit, r.reason, r.score # (True, 'hit', 0.968)
152
+ ```
153
+
154
+ **Opt-in: a verifier** (a second opinion on each hit the guards let through; served only on a yes). It catches
155
+ what similarity misses, so lower the threshold to 0.6 so it sees the grey band:
156
+
157
+ ```python
158
+ from pakka.verify import load_verifier
159
+
160
+ bot = CachedAnswerer(cache, my_llm, threshold=0.6, guard=check, policy=Policy(kb_version="v1"),
161
+ verify=load_verifier("jev")) # TypeSafe's Jev, needs TYPESAFE_API_KEY, passes at score 0.84
162
+ # or verify=load_verifier("openrouter:qwen/qwen3.7-flash") any OpenRouter model, needs OPENROUTER_API_KEY
163
+ # or verify=my_check any function (question, cached_question, cached_answer) -> bool
164
+ ```
165
+
166
+ **Opt-in: a better embedder.** With a CPU and a verifier, the fine-tuned MiniLM; with a GPU or Apple Silicon,
167
+ Qwen3-Embedding-0.6B:
168
+
169
+ ```python
170
+ from pakka.embed import GPU_EMBEDDER, TUNED_EMBEDDER
171
+
172
+ cache = NumpyCache(load_embedder(TUNED_EMBEDDER)) # same 9 ms; use with verify= and threshold=0.6
173
+ cache = NumpyCache(load_embedder(GPU_EMBEDDER)) # threshold=0.925 free, 0.6 with verify=
174
+ ```
175
+
176
+ **Measure your cache** on a labelled set, and fail CI when the false-hit bound regresses:
177
+
178
+ ```bash
179
+ pakka bench --data data/bench_v1.jsonl --guard --budget 0.10 --fail-above 0.25
180
+ pakka bench --data data/bench_v1.jsonl --guard --budget 0.05 --verify jev-1.14.0 --verify openrouter:qwen/qwen3.7-flash
181
+ ```
182
+
183
+ ## What pakka does
184
+
185
+ | Part | What it is |
186
+ |---|---|
187
+ | `pakka bench` | Replays a labelled question set through a cache. Reports hit rate and false hits with 95% Wilson intervals, picks the threshold on a dev split and reports on held-out, breaks errors down by trap type and language, and fails CI when the false-hit bound regresses |
188
+ | HinglishCacheBench (`data/`) | 1,059 support questions in English, Hinglish and Hindi with labelled traps (negation, entity, number, near intent, personal), LLM-written and labelled blind by a second LLM with human review, plus 315 messages written by 23 people ([dataset card](data/README.md)) |
189
+ | `pakka.guard` | Microsecond rule guards that veto a hit: negation (incl. nahi/nhi/nahin, नहीं), numbers and IDs, days and times (today/kal/आज, am/pm), opposite actions (on/off, buy/sell), swapped direction ("pdf to jpg"), and out-of-scope names |
190
+ | `pakka.verify` | Optional, off by default: a second opinion on hits the guards let through, from TypeSafe's Jev, any OpenRouter model, or your own function. PII is masked first; an error or timeout counts as a miss |
191
+ | `pakka.policy` + `pakka.pii` | Never stores Indian PII (Aadhaar with checksum, PAN, UPI, +91 mobiles, IFSC, cards, order IDs), keeps each user's answers in their own partition, retires answers when the FAQ changes, expires stored answers, queues shared answers for approval, and supports erasure requests |
192
+ | `pakka.testset`, `pakka.train` | Generate a stress-test set for your own FAQ with two LLMs, and fine-tune an embedder on it |
193
+ | Fine-tuned MiniLM (`TUNED_EMBEDDER`) | [`mohd010/pakka-minilm-l12-hinglish`](https://huggingface.co/mohd010/pakka-minilm-l12-hinglish): same size and speed as MiniLM, helps only with a verifier ([model card](docs/model_card_minilm_hinglish.md)) |
194
+ | Dukaan Bot (`examples/dukaan_bot`) | A small FAQ support bot for a fictional Indian store, used as the end-to-end test |
195
+
196
+ | Safety check | Without pakka | With pakka |
197
+ |---|---|---|
198
+ | Hits served from before an FAQ edit (persistent cache) | 100% | 0% |
199
+ | One user's LLM answer served to another user | yes | never (canary test, in memory and on Redis) |
200
+ | A planted answer reaching other users | yes | stays in the attacker's partition |
201
+
202
+ ## How a question flows
203
+
204
+ ```mermaid
205
+ flowchart TD
206
+ Q[Question + user_id] --> E[Embed and find the nearest entry<br/>in this user's partition or the shared FAQs,<br/>current FAQ version, not expired]
207
+ E --> T{Similarity at or above threshold?}
208
+ T -- no --> L[Call the LLM]
209
+ T -- yes --> G{Guards: negation, numbers, days and times,<br/>opposite actions, direction, out-of-scope names}
210
+ G -- veto --> L
211
+ G -- pass --> V{Optional verifier, PII masked:<br/>does the cached reply fit?}
212
+ V -- no, error or timeout --> L
213
+ V -- yes, or no verifier --> H[Serve the cached answer]
214
+ L --> P{Contains PII?}
215
+ P -- yes --> N[Answer, never store]
216
+ P -- no --> S{Shared or per user?}
217
+ S -- user --> U[Store in the user's partition with a TTL]
218
+ S -- shared --> A[Queue for approval]
219
+ ```
220
+
221
+ ## Choosing an embedder
222
+
223
+ | | MiniLM-L12 (default) | Fine-tuned MiniLM (opt-in) | Qwen3-Embedding-0.6B (GPU option) |
224
+ |---|---|---|---|
225
+ | Use when | any CPU, no API calls | a CPU bot with a verifier | a GPU or Apple Silicon |
226
+ | Download / memory (process peak) | 0.48 GB / ~0.7 GB | 0.47 GB / - | 1.2 GB / ~1.0 GB |
227
+ | CPU, 8 threads (median / p90) | 9 / 10 ms | 9 ms | 235 / 312 ms |
228
+ | CPU, 2 threads, like a small VPS (median / p90) | 8 / 8 ms | - | 538 / 834 ms |
229
+ | M2 GPU (median) | ~9 ms | - | ~36 ms |
230
+ | Threshold free (`pakka.embed.THRESHOLDS`) / with a verifier | 0.79 / 0.6 | 0.906 / 0.6 | 0.925 / 0.6 |
231
+ | Human test, free / + Jev | 2 / 1 → 19 / 0 | 9 / 2 → 54 / 1 | 11 / 0 → 80 / 1 |
232
+
233
+ - **The fine-tuned MiniLM helps only with a verifier.** Fine-tuning lifts real messages above the verifier's 0.6
234
+ floor (165 of 258 instead of 26); the verifier then sorts them. Without one it is no better than MiniLM and worse
235
+ on CLINC150. Trained on public dev and train splits only, measured on data it never saw.
236
+ - **Qwen3-0.6B with a verifier is the strongest setup** but takes a quarter to half a second a question on a CPU.
237
+ - Mean P-CHR AUC over the public datasets: 0.571 for MiniLM, 0.647 for Qwen3-0.6B. A threshold belongs to one
238
+ embedder; for any other model, pick one with `pakka bench --embedder <model>`.
239
+ - In Dukaan Bot: `PAKKA_EMBEDDER=Qwen/Qwen3-Embedding-0.6B`, or
240
+ `PAKKA_EMBEDDER=mohd010/pakka-minilm-l12-hinglish PAKKA_VERIFY=jev` (or `PAKKA_VERIFY=openrouter:<model>`); the
241
+ threshold follows, and drops to 0.6 when a verifier is on.
242
+
243
+ ## The verifier, in detail
244
+
245
+ - Off by default: pakka works fully offline without one. It asks one question about each hit the guards let
246
+ through, "would the reply for the cached question also answer this one?".
247
+ - The verifier only ever sees text masked with `pakka.pii.mask` (Aadhaar, PAN, phone numbers, order IDs and so on).
248
+ - One try with a 5-second timeout (`load_verifier(..., timeout=)`). An error or a timeout counts as a miss, so the
249
+ LLM answers and no unchecked hit is served. `Result.reason` says `blocked:verify` or `blocked:verify_error`.
250
+ - **Jev gives a score, the cheap LLM only yes or no.** Jev can be tightened to a 5% budget; the cheap LLM sits at
251
+ about 10%, and on Bitext's near-intent pairs it said yes too easily (26.5% wrong). Jev: ~0.3 s and ~$1.75 per
252
+ 100k checks. `qwen/qwen3.7-flash`: ~0.9 s and ~$0.60 per 100k, the cheapest of five tried and as accurate on
253
+ v1 dev. Measure your own pick with `pakka bench --verify` before trusting it.
254
+
255
+ ## Run everything
256
+
257
+ ```bash
258
+ python3.12 -m venv .venv && .venv/bin/pip install -e ".[redis,bot,dev,train,verify]"
259
+ .venv/bin/python -m pytest # unit tests, under a second
260
+ docker run -d --name pakka-redis -p 6380:6379 redis:8.2.2-alpine3.22
261
+ .venv/bin/python -m pytest -m integration # real model + Redis
262
+ echo "OPENROUTER_API_KEY=..." > .env # for the bot and e2e test
263
+ .venv/bin/python -m examples.dukaan_bot.chat --user ravi # chat with Dukaan Bot
264
+ .venv/bin/python scripts/public_bench.py # every public-dataset table (hours the first time)
265
+ .venv/bin/python scripts/phase10_bot.py --setups free,tuned+llm,tuned+jev,qwen+jev # the real-bot run
266
+ .venv/bin/pip install matplotlib && .venv/bin/python scripts/charts.py # the charts above
267
+ ```
268
+
269
+ More commands (building the benchmark, fine-tuning, the approval queue, the stale-answer demo) are in
270
+ [docs/results.md](docs/results.md); reproduce steps per experiment are in [docs/report.md](docs/report.md#reproduce).
271
+
272
+ ## Limits
273
+
274
+ - **Labels are noisy.** Three LLM judges overturn 6–46% of sampled hit labels per public dataset; intent
275
+ datasets label intent, not answers. Numbers on them are estimates, not ground truth.
276
+ - **The human test set is small** (258 messages, 23 writers, one shop's 30 FAQs), so its 95% bounds are wide.
277
+ - **The best results need an API call.** Off-the-shelf cross-encoder verifiers made every result worse; the
278
+ verifiers that help are Jev or an LLM, so they are optional and off by default.
279
+ - With the default MiniLM-L12, most real messages score below a verifier's 0.6 floor, so a verifier helps it less.
280
+ - PII detection is regex plus checksums and doesn't catch names, so a verifier can see a name. Partitions isolate
281
+ answers but not timing: a hit is still faster than a miss. `/forget` in the demo bot has no authentication.
282
+ - HinglishCacheBench covers one domain (Indian e-commerce support) with 30 FAQs.
283
+
284
+ **Disclosure.** Jev is made by [TypeSafe](https://typesafe.ai). Pakka has no link to TypeSafe: no job,
285
+ sponsorship or free credits. Jev was used as a regular API, and the cheap OpenRouter verifier is measured next to
286
+ it so the results don't depend on one vendor.
287
+
288
+ ## License
289
+
290
+ Code: MIT. Dataset: CC BY 4.0. Fine-tuned model: Apache-2.0.
pakka-0.1.0/README.md ADDED
@@ -0,0 +1,259 @@
1
+ # pakka
2
+
3
+ **Measure and cut the wrong answers a semantic cache serves, in English and Hinglish support bots.**
4
+
5
+ A semantic cache answers a repeat question from memory when its meaning matches an earlier one. Redis, Azure,
6
+ Kong, LiteLLM and most gateways ship one. None of them tells you **how often the cache serves a confident,
7
+ wrong answer.** Pakka measures that, blocks the common causes, and keeps the cache from leaking or going stale.
8
+ It is a Python library plus a benchmark, tested on nine public datasets other people built and labelled, plus
9
+ 315 messages real people wrote.
10
+
11
+ *Pakka* is Hinglish for "sure": *pakka sure hai?*
12
+
13
+ ## Results
14
+
15
+ ![RedisVL's default cache on ten datasets: more than 5% of its hits are wrong on six of them, up to 66%](docs/img/redisvl_defaults.png)
16
+
17
+ - **Defaults are not safe.** RedisVL's out-of-the-box cache (`langcache-embed-v1` at similarity 0.90) serves more
18
+ than 5% wrong answers on 6 of 10 datasets, from 7% (vCache LmArena) to 66% (Hinglish-TOP). No single threshold
19
+ carries over between datasets or embedders.
20
+ - **In a real bot, a cache saves money but is wrong far more often than the LLM.** Dukaan Bot on 4,000 held-out
21
+ Bitext questions, every served reply checked by three LLM judges: the cache answers 10–52% of questions and
22
+ cuts LLM spend by 10–51%, but 15–27% of the cached answers are wrong, against 3–4% for the LLM's own answers
23
+ to the same questions (6–9 times as often).
24
+ - **Real Hinglish is much harder than LLM-written Hinglish.** On 258 messages people wrote, the free default
25
+ answers 3 (1 wrong). v1's LLM-written test made pakka look far better than this; see
26
+ [what v1 got wrong](docs/report.md#limitations).
27
+
28
+ **The real-bot run** (Phase 10): 4,000 Bitext test questions, Bitext's 27 e-commerce answers as the knowledge
29
+ base, `claude-haiku-5.5` as the LLM. Wrong = judged wrong by the LLM judges, not by intent labels.
30
+
31
+ | Setup | Answered from cache | Wrong answers served (95% upper) | Hit time p50 | LLM spend saved, verifier paid |
32
+ |---|---|---|---|---|
33
+ | No cache | 0 | LLM's own: 3–4% | 1,248 ms | - |
34
+ | **MiniLM-L12 (default), free** | 9.8% | 103 of 394: 26% (31%) | 9 ms | 9.9% |
35
+ | Fine-tuned MiniLM + cheap LLM verifier | 52.3% | 554 of 2,091: 26.5% (28%) | 777 ms | 51.3% |
36
+ | Fine-tuned MiniLM + Jev verifier | 30.9% | 187 of 1,236: 15% (17%) | 326 ms | 27.8% |
37
+ | Qwen3-0.6B (GPU) + Jev verifier | 38.1% | 254 of 1,525: 17% (19%) | 379 ms | 35.1% |
38
+
39
+ ![Share answered from cache against share of cached answers judged wrong, for the four setups; the LLM's own wrong rate is 3-4%](docs/img/bot_run.png)
40
+
41
+ Most of those wrong answers come from a few FAQ answers that only fit their own seed question: Bitext's "cancel
42
+ order" answer just says "please ask your question" (27 of the free setup's 103, 61 of Jev's 187). **Seed a cache
43
+ with answers that stand on their own.**
44
+
45
+ **Our own sets**, guards on, threshold picked on v1's dev split. Cells are right / wrong answers served from cache
46
+ (95% upper bound on the false-hit rate). v1 test is 526 LLM-written questions; human test is 258 messages real
47
+ people wrote (80% romanized Hinglish), the harder and more honest one.
48
+
49
+ | Setup | v1 test (526) | Human test (258) | Hinglish caught (human) |
50
+ |---|---|---|---|
51
+ | **Free:** MiniLM-L12 (default), 0.79, 10% budget | 47 / 5 (20.6%) | 2 / 1 (79.2%) | 0/184 |
52
+ | **Free:** Qwen3-Embedding-0.6B (GPU option), 0.925, 10% budget | 102 / 8 (13.7%) | 11 / 0 (25.9%) | 6/184 |
53
+ | MiniLM-L12 + cheap LLM verifier | 118 / 6 (10.2%) | 23 / 0 (14.3%) | 12/184 |
54
+ | MiniLM-L12 + Jev verifier | 96 / 1 (5.6%) | 19 / 0 (16.8%) | 10/184 |
55
+ | Fine-tuned MiniLM-L12 (opt-in) + Jev | 148 / 3 (5.7%) | 54 / 1 (9.6%) | 44/184 |
56
+ | Qwen3-0.6B + cheap LLM | 231 / 13 (8.9%) | 113 / 9 (13.4%) | 97/184 |
57
+ | Qwen3-0.6B + Jev | 181 / 3 (4.7%) | 80 / 1 (6.7%) | 67/184 |
58
+
59
+ Without a verifier no threshold fits a 5% budget for either embedder. Everything: [the tech report](docs/report.md),
60
+ [public results](docs/public_results.md) (every table, with a one-command reproduce) and the
61
+ [lab notebook](docs/results.md).
62
+
63
+ ## When pakka helps, and when it doesn't
64
+
65
+ **It helps when:**
66
+ - you run a **high-volume FAQ or support bot** where many people ask the same few things;
67
+ - your users write **Hinglish or Hindi**: the guards read *nahi*, *kal*, *band karo*, Devanagari digits and the like,
68
+ and the benchmark tells you how your embedder does on real Hinglish;
69
+ - you are **cost-sensitive**: free hits take 9 ms on a CPU instead of a 1.2 s LLM call;
70
+ - you are **multi-tenant with PII**: per-user partitions, a PII gate that never stores Aadhaar, PAN, UPI, phone or
71
+ order IDs, and an approval queue for shared answers.
72
+
73
+ **It doesn't help, or isn't worth it, when:**
74
+ - prompts are **unique or long** (chat, coding, agents): few repeats, and long texts all look alike to an embedder;
75
+ - answers are **personal** ("where is my order?"): the answer depends on the user, not the question;
76
+ - answers **change fast** (stock, prices, live status): you'll spend your time invalidating;
77
+ - traffic is **low**: there is little to reuse, and you can't measure the cache on your own traffic;
78
+ - the domain has **zero tolerance** for a wrong answer (medical, legal, money), unless a human approves every
79
+ answer the cache may serve. Even the best setup above is wrong on 15% of its hits on Bitext.
80
+
81
+ ## Install
82
+
83
+ ```bash
84
+ pip install "pakka @ git+https://github.com/mohammedp010/pakka" # default: free, offline, CPU
85
+ pip install "pakka[verify] @ git+https://github.com/mohammedp010/pakka" # + the optional verifier's HTTP client
86
+ ```
87
+
88
+ Not on PyPI yet. From a clone: `pip install -e ".[verify]"`.
89
+
90
+ ## Quickstart
91
+
92
+ **Default: free, offline, MiniLM-L12 on a CPU.** Guard a cache you already have:
93
+
94
+ ```python
95
+ from pakka.guard import check
96
+
97
+ check("refund nahi mila", "How long does a refund take?") # "negation"
98
+ check("Is Cash on Delivery available in Nepal?", "Is Cash on Delivery available?") # "entity"
99
+ check("COD available hai kya?", "Is Cash on Delivery available?") # None: safe to serve
100
+ ```
101
+
102
+ Or use the full flow:
103
+
104
+ ```python
105
+ from pakka.cache import NumpyCache # or pakka.redis_cache.RedisVLCache
106
+ from pakka.embed import load_embedder
107
+ from pakka.flow import CachedAnswerer
108
+ from pakka.guard import check
109
+ from pakka.policy import GLOBAL, Policy
110
+
111
+ def my_llm(question): # your LLM call: returns (answer, tokens used)
112
+ ...
113
+
114
+ cache = NumpyCache(load_embedder()) # MiniLM-L12
115
+ cache.store("How long does a refund take?", "Refunds take 5-7 business days.",
116
+ {"id": "faq_01", "partition": GLOBAL, "kb_version": "v1"})
117
+
118
+ bot = CachedAnswerer(cache, my_llm, threshold=0.79, guard=check, policy=Policy(kb_version="v1"))
119
+ r = bot.ask("How many days does a refund take?", partition="user-42")
120
+ r.hit, r.reason, r.score # (True, 'hit', 0.968)
121
+ ```
122
+
123
+ **Opt-in: a verifier** (a second opinion on each hit the guards let through; served only on a yes). It catches
124
+ what similarity misses, so lower the threshold to 0.6 so it sees the grey band:
125
+
126
+ ```python
127
+ from pakka.verify import load_verifier
128
+
129
+ bot = CachedAnswerer(cache, my_llm, threshold=0.6, guard=check, policy=Policy(kb_version="v1"),
130
+ verify=load_verifier("jev")) # TypeSafe's Jev, needs TYPESAFE_API_KEY, passes at score 0.84
131
+ # or verify=load_verifier("openrouter:qwen/qwen3.7-flash") any OpenRouter model, needs OPENROUTER_API_KEY
132
+ # or verify=my_check any function (question, cached_question, cached_answer) -> bool
133
+ ```
134
+
135
+ **Opt-in: a better embedder.** With a CPU and a verifier, the fine-tuned MiniLM; with a GPU or Apple Silicon,
136
+ Qwen3-Embedding-0.6B:
137
+
138
+ ```python
139
+ from pakka.embed import GPU_EMBEDDER, TUNED_EMBEDDER
140
+
141
+ cache = NumpyCache(load_embedder(TUNED_EMBEDDER)) # same 9 ms; use with verify= and threshold=0.6
142
+ cache = NumpyCache(load_embedder(GPU_EMBEDDER)) # threshold=0.925 free, 0.6 with verify=
143
+ ```
144
+
145
+ **Measure your cache** on a labelled set, and fail CI when the false-hit bound regresses:
146
+
147
+ ```bash
148
+ pakka bench --data data/bench_v1.jsonl --guard --budget 0.10 --fail-above 0.25
149
+ pakka bench --data data/bench_v1.jsonl --guard --budget 0.05 --verify jev-1.14.0 --verify openrouter:qwen/qwen3.7-flash
150
+ ```
151
+
152
+ ## What pakka does
153
+
154
+ | Part | What it is |
155
+ |---|---|
156
+ | `pakka bench` | Replays a labelled question set through a cache. Reports hit rate and false hits with 95% Wilson intervals, picks the threshold on a dev split and reports on held-out, breaks errors down by trap type and language, and fails CI when the false-hit bound regresses |
157
+ | HinglishCacheBench (`data/`) | 1,059 support questions in English, Hinglish and Hindi with labelled traps (negation, entity, number, near intent, personal), LLM-written and labelled blind by a second LLM with human review, plus 315 messages written by 23 people ([dataset card](data/README.md)) |
158
+ | `pakka.guard` | Microsecond rule guards that veto a hit: negation (incl. nahi/nhi/nahin, नहीं), numbers and IDs, days and times (today/kal/आज, am/pm), opposite actions (on/off, buy/sell), swapped direction ("pdf to jpg"), and out-of-scope names |
159
+ | `pakka.verify` | Optional, off by default: a second opinion on hits the guards let through, from TypeSafe's Jev, any OpenRouter model, or your own function. PII is masked first; an error or timeout counts as a miss |
160
+ | `pakka.policy` + `pakka.pii` | Never stores Indian PII (Aadhaar with checksum, PAN, UPI, +91 mobiles, IFSC, cards, order IDs), keeps each user's answers in their own partition, retires answers when the FAQ changes, expires stored answers, queues shared answers for approval, and supports erasure requests |
161
+ | `pakka.testset`, `pakka.train` | Generate a stress-test set for your own FAQ with two LLMs, and fine-tune an embedder on it |
162
+ | Fine-tuned MiniLM (`TUNED_EMBEDDER`) | [`mohd010/pakka-minilm-l12-hinglish`](https://huggingface.co/mohd010/pakka-minilm-l12-hinglish): same size and speed as MiniLM, helps only with a verifier ([model card](docs/model_card_minilm_hinglish.md)) |
163
+ | Dukaan Bot (`examples/dukaan_bot`) | A small FAQ support bot for a fictional Indian store, used as the end-to-end test |
164
+
165
+ | Safety check | Without pakka | With pakka |
166
+ |---|---|---|
167
+ | Hits served from before an FAQ edit (persistent cache) | 100% | 0% |
168
+ | One user's LLM answer served to another user | yes | never (canary test, in memory and on Redis) |
169
+ | A planted answer reaching other users | yes | stays in the attacker's partition |
170
+
171
+ ## How a question flows
172
+
173
+ ```mermaid
174
+ flowchart TD
175
+ Q[Question + user_id] --> E[Embed and find the nearest entry<br/>in this user's partition or the shared FAQs,<br/>current FAQ version, not expired]
176
+ E --> T{Similarity at or above threshold?}
177
+ T -- no --> L[Call the LLM]
178
+ T -- yes --> G{Guards: negation, numbers, days and times,<br/>opposite actions, direction, out-of-scope names}
179
+ G -- veto --> L
180
+ G -- pass --> V{Optional verifier, PII masked:<br/>does the cached reply fit?}
181
+ V -- no, error or timeout --> L
182
+ V -- yes, or no verifier --> H[Serve the cached answer]
183
+ L --> P{Contains PII?}
184
+ P -- yes --> N[Answer, never store]
185
+ P -- no --> S{Shared or per user?}
186
+ S -- user --> U[Store in the user's partition with a TTL]
187
+ S -- shared --> A[Queue for approval]
188
+ ```
189
+
190
+ ## Choosing an embedder
191
+
192
+ | | MiniLM-L12 (default) | Fine-tuned MiniLM (opt-in) | Qwen3-Embedding-0.6B (GPU option) |
193
+ |---|---|---|---|
194
+ | Use when | any CPU, no API calls | a CPU bot with a verifier | a GPU or Apple Silicon |
195
+ | Download / memory (process peak) | 0.48 GB / ~0.7 GB | 0.47 GB / - | 1.2 GB / ~1.0 GB |
196
+ | CPU, 8 threads (median / p90) | 9 / 10 ms | 9 ms | 235 / 312 ms |
197
+ | CPU, 2 threads, like a small VPS (median / p90) | 8 / 8 ms | - | 538 / 834 ms |
198
+ | M2 GPU (median) | ~9 ms | - | ~36 ms |
199
+ | Threshold free (`pakka.embed.THRESHOLDS`) / with a verifier | 0.79 / 0.6 | 0.906 / 0.6 | 0.925 / 0.6 |
200
+ | Human test, free / + Jev | 2 / 1 → 19 / 0 | 9 / 2 → 54 / 1 | 11 / 0 → 80 / 1 |
201
+
202
+ - **The fine-tuned MiniLM helps only with a verifier.** Fine-tuning lifts real messages above the verifier's 0.6
203
+ floor (165 of 258 instead of 26); the verifier then sorts them. Without one it is no better than MiniLM and worse
204
+ on CLINC150. Trained on public dev and train splits only, measured on data it never saw.
205
+ - **Qwen3-0.6B with a verifier is the strongest setup** but takes a quarter to half a second a question on a CPU.
206
+ - Mean P-CHR AUC over the public datasets: 0.571 for MiniLM, 0.647 for Qwen3-0.6B. A threshold belongs to one
207
+ embedder; for any other model, pick one with `pakka bench --embedder <model>`.
208
+ - In Dukaan Bot: `PAKKA_EMBEDDER=Qwen/Qwen3-Embedding-0.6B`, or
209
+ `PAKKA_EMBEDDER=mohd010/pakka-minilm-l12-hinglish PAKKA_VERIFY=jev` (or `PAKKA_VERIFY=openrouter:<model>`); the
210
+ threshold follows, and drops to 0.6 when a verifier is on.
211
+
212
+ ## The verifier, in detail
213
+
214
+ - Off by default: pakka works fully offline without one. It asks one question about each hit the guards let
215
+ through, "would the reply for the cached question also answer this one?".
216
+ - The verifier only ever sees text masked with `pakka.pii.mask` (Aadhaar, PAN, phone numbers, order IDs and so on).
217
+ - One try with a 5-second timeout (`load_verifier(..., timeout=)`). An error or a timeout counts as a miss, so the
218
+ LLM answers and no unchecked hit is served. `Result.reason` says `blocked:verify` or `blocked:verify_error`.
219
+ - **Jev gives a score, the cheap LLM only yes or no.** Jev can be tightened to a 5% budget; the cheap LLM sits at
220
+ about 10%, and on Bitext's near-intent pairs it said yes too easily (26.5% wrong). Jev: ~0.3 s and ~$1.75 per
221
+ 100k checks. `qwen/qwen3.7-flash`: ~0.9 s and ~$0.60 per 100k, the cheapest of five tried and as accurate on
222
+ v1 dev. Measure your own pick with `pakka bench --verify` before trusting it.
223
+
224
+ ## Run everything
225
+
226
+ ```bash
227
+ python3.12 -m venv .venv && .venv/bin/pip install -e ".[redis,bot,dev,train,verify]"
228
+ .venv/bin/python -m pytest # unit tests, under a second
229
+ docker run -d --name pakka-redis -p 6380:6379 redis:8.2.2-alpine3.22
230
+ .venv/bin/python -m pytest -m integration # real model + Redis
231
+ echo "OPENROUTER_API_KEY=..." > .env # for the bot and e2e test
232
+ .venv/bin/python -m examples.dukaan_bot.chat --user ravi # chat with Dukaan Bot
233
+ .venv/bin/python scripts/public_bench.py # every public-dataset table (hours the first time)
234
+ .venv/bin/python scripts/phase10_bot.py --setups free,tuned+llm,tuned+jev,qwen+jev # the real-bot run
235
+ .venv/bin/pip install matplotlib && .venv/bin/python scripts/charts.py # the charts above
236
+ ```
237
+
238
+ More commands (building the benchmark, fine-tuning, the approval queue, the stale-answer demo) are in
239
+ [docs/results.md](docs/results.md); reproduce steps per experiment are in [docs/report.md](docs/report.md#reproduce).
240
+
241
+ ## Limits
242
+
243
+ - **Labels are noisy.** Three LLM judges overturn 6–46% of sampled hit labels per public dataset; intent
244
+ datasets label intent, not answers. Numbers on them are estimates, not ground truth.
245
+ - **The human test set is small** (258 messages, 23 writers, one shop's 30 FAQs), so its 95% bounds are wide.
246
+ - **The best results need an API call.** Off-the-shelf cross-encoder verifiers made every result worse; the
247
+ verifiers that help are Jev or an LLM, so they are optional and off by default.
248
+ - With the default MiniLM-L12, most real messages score below a verifier's 0.6 floor, so a verifier helps it less.
249
+ - PII detection is regex plus checksums and doesn't catch names, so a verifier can see a name. Partitions isolate
250
+ answers but not timing: a hit is still faster than a miss. `/forget` in the demo bot has no authentication.
251
+ - HinglishCacheBench covers one domain (Indian e-commerce support) with 30 FAQs.
252
+
253
+ **Disclosure.** Jev is made by [TypeSafe](https://typesafe.ai). Pakka has no link to TypeSafe: no job,
254
+ sponsorship or free credits. Jev was used as a regular API, and the cheap OpenRouter verifier is measured next to
255
+ it so the results don't depend on one vendor.
256
+
257
+ ## License
258
+
259
+ Code: MIT. Dataset: CC BY 4.0. Fine-tuned model: Apache-2.0.
@@ -0,0 +1 @@
1
+ """Pakka: tests, guards and safety rules for semantic caches."""
@@ -0,0 +1,44 @@
1
+ """Approve answers waiting to enter the shared cache.
2
+
3
+ python -m pakka.approve data/pending.jsonl --to data/approved.jsonl
4
+
5
+ y = approve, n = discard, s = keep for later, q = quit. Both files are saved after every answer.
6
+ """
7
+
8
+ import argparse
9
+ import json
10
+ from pathlib import Path
11
+
12
+
13
+ def _write(path, rows):
14
+ path.write_text("".join(json.dumps(r, ensure_ascii=False) + "\n" for r in rows))
15
+
16
+
17
+ def main(argv=None):
18
+ p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
19
+ p.add_argument("pending", type=Path)
20
+ p.add_argument("--to", type=Path, default=Path("data/approved.jsonl"))
21
+ args = p.parse_args(argv)
22
+
23
+ rows = [json.loads(line) for line in args.pending.read_text().splitlines() if line.strip()]
24
+ kept = []
25
+ for i, row in enumerate(rows):
26
+ print(f"\n[{i + 1}/{len(rows)}] Q: {row['question']}\n A: {row['answer']}")
27
+ answer = ""
28
+ while answer not in ("y", "n", "s", "q"):
29
+ answer = input("approve? [y/n/s/q] ").strip().lower()
30
+ if answer == "q":
31
+ kept += rows[i:]
32
+ break
33
+ if answer == "y":
34
+ with open(args.to, "a") as f:
35
+ f.write(json.dumps(row, ensure_ascii=False) + "\n")
36
+ if answer == "s":
37
+ kept.append(row)
38
+ _write(args.pending, kept + rows[i + 1:])
39
+ _write(args.pending, kept)
40
+ print(f"\n{len(kept)} left in {args.pending}")
41
+
42
+
43
+ if __name__ == "__main__":
44
+ main()