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.
- pakka-0.1.0/LICENSE +21 -0
- pakka-0.1.0/PKG-INFO +290 -0
- pakka-0.1.0/README.md +259 -0
- pakka-0.1.0/pakka/__init__.py +1 -0
- pakka-0.1.0/pakka/approve.py +44 -0
- pakka-0.1.0/pakka/bench.py +380 -0
- pakka-0.1.0/pakka/cache.py +83 -0
- pakka-0.1.0/pakka/cli.py +20 -0
- pakka-0.1.0/pakka/embed.py +104 -0
- pakka-0.1.0/pakka/flow.py +83 -0
- pakka-0.1.0/pakka/guard.py +238 -0
- pakka-0.1.0/pakka/pii.py +81 -0
- pakka-0.1.0/pakka/policy.py +45 -0
- pakka-0.1.0/pakka/redis_cache.py +62 -0
- pakka-0.1.0/pakka/review.py +74 -0
- pakka-0.1.0/pakka/testset.py +158 -0
- pakka-0.1.0/pakka/train.py +47 -0
- pakka-0.1.0/pakka/verify.py +128 -0
- pakka-0.1.0/pakka.egg-info/PKG-INFO +290 -0
- pakka-0.1.0/pakka.egg-info/SOURCES.txt +48 -0
- pakka-0.1.0/pakka.egg-info/dependency_links.txt +1 -0
- pakka-0.1.0/pakka.egg-info/entry_points.txt +2 -0
- pakka-0.1.0/pakka.egg-info/requires.txt +19 -0
- pakka-0.1.0/pakka.egg-info/top_level.txt +1 -0
- pakka-0.1.0/pyproject.toml +43 -0
- pakka-0.1.0/setup.cfg +4 -0
- pakka-0.1.0/tests/test_approve.py +18 -0
- pakka-0.1.0/tests/test_bench.py +329 -0
- pakka-0.1.0/tests/test_bot.py +148 -0
- pakka-0.1.0/tests/test_cache.py +65 -0
- pakka-0.1.0/tests/test_cli.py +19 -0
- pakka-0.1.0/tests/test_data.py +29 -0
- pakka-0.1.0/tests/test_e2e.py +23 -0
- pakka-0.1.0/tests/test_e2e_verify.py +20 -0
- pakka-0.1.0/tests/test_fetch_public.py +210 -0
- pakka-0.1.0/tests/test_flow.py +129 -0
- pakka-0.1.0/tests/test_guard.py +226 -0
- pakka-0.1.0/tests/test_ingest_human.py +164 -0
- pakka-0.1.0/tests/test_integration.py +102 -0
- pakka-0.1.0/tests/test_label_audit.py +68 -0
- pakka-0.1.0/tests/test_llm.py +46 -0
- pakka-0.1.0/tests/test_phase10.py +45 -0
- pakka-0.1.0/tests/test_phase9.py +28 -0
- pakka-0.1.0/tests/test_pii.py +53 -0
- pakka-0.1.0/tests/test_policy.py +79 -0
- pakka-0.1.0/tests/test_replay.py +32 -0
- pakka-0.1.0/tests/test_review.py +23 -0
- pakka-0.1.0/tests/test_testset.py +83 -0
- pakka-0.1.0/tests/test_train.py +20 -0
- 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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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()
|