maskflow-sdk 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.
- maskflow_sdk-0.1.0/.gitignore +12 -0
- maskflow_sdk-0.1.0/.python-version +1 -0
- maskflow_sdk-0.1.0/PKG-INFO +78 -0
- maskflow_sdk-0.1.0/README.md +67 -0
- maskflow_sdk-0.1.0/pyproject.toml +28 -0
- maskflow_sdk-0.1.0/src/maskflow/__init__.py +13 -0
- maskflow_sdk-0.1.0/src/maskflow/sdk.py +23 -0
- maskflow_sdk-0.1.0/tests/test_sdk.py +51 -0
- maskflow_sdk-0.1.0/uv.lock +2066 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maskflow-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Mask PII before it reaches an LLM, unmask the response. Works with any provider.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Requires-Dist: maskflow-core<0.2,>=0.1.0
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# maskflow
|
|
13
|
+
|
|
14
|
+
Mask PII before it reaches an LLM. Unmask the response. Works with any provider.
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from maskflow import mask_and_call
|
|
18
|
+
|
|
19
|
+
def call_claude(masked_prompt: str) -> str:
|
|
20
|
+
return anthropic_client.messages.create(
|
|
21
|
+
model="claude-sonnet-5",
|
|
22
|
+
max_tokens=1024,
|
|
23
|
+
messages=[{"role": "user", "content": masked_prompt}],
|
|
24
|
+
).content[0].text
|
|
25
|
+
|
|
26
|
+
response = mask_and_call(
|
|
27
|
+
"Hi, I'm Jane Doe (jane@example.com). My order shipped to 123 Main St but never arrived.",
|
|
28
|
+
call_claude,
|
|
29
|
+
)
|
|
30
|
+
# Claude only ever sees "Hi, I'm <PERSON_NAME_1> (<EMAIL_1>). My order shipped to
|
|
31
|
+
# <ADDRESS_1> but never arrived." -- response comes back with the real values restored.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install maskflow-sdk
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Why this shape
|
|
41
|
+
|
|
42
|
+
`mask_and_call` takes a plain function, not a specific provider's client. You write the one line
|
|
43
|
+
that actually calls your LLM (Claude, OpenAI, Gemini, a local model, anything) -- maskflow never
|
|
44
|
+
parses or depends on any provider's SDK, so it doesn't break when a provider changes their API and
|
|
45
|
+
works with providers it's never heard of.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
response = mask_and_call(prompt, lambda masked: my_llm_client.generate(masked))
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Lower-level API
|
|
52
|
+
|
|
53
|
+
For more control than the wrapper gives, `mask`/`unmask` are available directly and are pure,
|
|
54
|
+
stateless functions -- no files, no database. Persisting the mapping between calls is your
|
|
55
|
+
responsibility.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from maskflow import mask, unmask
|
|
59
|
+
|
|
60
|
+
result = mask("Email me at alice@example.com.")
|
|
61
|
+
result.masked_text # "Email me at <EMAIL_1>."
|
|
62
|
+
result.mapping # {"<EMAIL_1>": "alice@example.com"}
|
|
63
|
+
|
|
64
|
+
unmask(result.masked_text, result.mapping) # original text, restored
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## What gets detected
|
|
68
|
+
|
|
69
|
+
Email, phone, SSN, credit card, IP address, AWS access key, API key / generic secret, JWT, IBAN,
|
|
70
|
+
street address, person name, date of birth -- via regex + structural validation (Luhn, mod-97,
|
|
71
|
+
etc.) plus spaCy NER for names and dates, with keyword-context confidence boosting. See
|
|
72
|
+
[`maskflow-core`](../../core) for detection internals.
|
|
73
|
+
|
|
74
|
+
## Tests
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
uv run pytest
|
|
78
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# maskflow
|
|
2
|
+
|
|
3
|
+
Mask PII before it reaches an LLM. Unmask the response. Works with any provider.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from maskflow import mask_and_call
|
|
7
|
+
|
|
8
|
+
def call_claude(masked_prompt: str) -> str:
|
|
9
|
+
return anthropic_client.messages.create(
|
|
10
|
+
model="claude-sonnet-5",
|
|
11
|
+
max_tokens=1024,
|
|
12
|
+
messages=[{"role": "user", "content": masked_prompt}],
|
|
13
|
+
).content[0].text
|
|
14
|
+
|
|
15
|
+
response = mask_and_call(
|
|
16
|
+
"Hi, I'm Jane Doe (jane@example.com). My order shipped to 123 Main St but never arrived.",
|
|
17
|
+
call_claude,
|
|
18
|
+
)
|
|
19
|
+
# Claude only ever sees "Hi, I'm <PERSON_NAME_1> (<EMAIL_1>). My order shipped to
|
|
20
|
+
# <ADDRESS_1> but never arrived." -- response comes back with the real values restored.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install maskflow-sdk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Why this shape
|
|
30
|
+
|
|
31
|
+
`mask_and_call` takes a plain function, not a specific provider's client. You write the one line
|
|
32
|
+
that actually calls your LLM (Claude, OpenAI, Gemini, a local model, anything) -- maskflow never
|
|
33
|
+
parses or depends on any provider's SDK, so it doesn't break when a provider changes their API and
|
|
34
|
+
works with providers it's never heard of.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
response = mask_and_call(prompt, lambda masked: my_llm_client.generate(masked))
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Lower-level API
|
|
41
|
+
|
|
42
|
+
For more control than the wrapper gives, `mask`/`unmask` are available directly and are pure,
|
|
43
|
+
stateless functions -- no files, no database. Persisting the mapping between calls is your
|
|
44
|
+
responsibility.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from maskflow import mask, unmask
|
|
48
|
+
|
|
49
|
+
result = mask("Email me at alice@example.com.")
|
|
50
|
+
result.masked_text # "Email me at <EMAIL_1>."
|
|
51
|
+
result.mapping # {"<EMAIL_1>": "alice@example.com"}
|
|
52
|
+
|
|
53
|
+
unmask(result.masked_text, result.mapping) # original text, restored
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## What gets detected
|
|
57
|
+
|
|
58
|
+
Email, phone, SSN, credit card, IP address, AWS access key, API key / generic secret, JWT, IBAN,
|
|
59
|
+
street address, person name, date of birth -- via regex + structural validation (Luhn, mod-97,
|
|
60
|
+
etc.) plus spaCy NER for names and dates, with keyword-context confidence boosting. See
|
|
61
|
+
[`maskflow-core`](../../core) for detection internals.
|
|
62
|
+
|
|
63
|
+
## Tests
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv run pytest
|
|
67
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "maskflow-sdk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Mask PII before it reaches an LLM, unmask the response. Works with any provider."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
dependencies = [
|
|
9
|
+
"maskflow-core>=0.1.0,<0.2",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
dev = [
|
|
14
|
+
"pytest>=8.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[tool.uv.sources]
|
|
18
|
+
maskflow-core = { path = "../../core", editable = true }
|
|
19
|
+
|
|
20
|
+
[tool.uv]
|
|
21
|
+
package = true
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["hatchling"]
|
|
25
|
+
build-backend = "hatchling.build"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["src/maskflow"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
from maskflow_core import mask, unmask
|
|
4
|
+
from maskflow_core.detection import DEFAULT_MIN_CONFIDENCE
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def mask_and_call(
|
|
8
|
+
prompt: str,
|
|
9
|
+
call_fn: Callable[[str], str],
|
|
10
|
+
min_confidence: float = DEFAULT_MIN_CONFIDENCE,
|
|
11
|
+
) -> str:
|
|
12
|
+
"""Mask PII in `prompt`, pass the masked text to `call_fn`, and restore the
|
|
13
|
+
original values in whatever `call_fn` returns.
|
|
14
|
+
|
|
15
|
+
`call_fn` is any function that takes a string and returns a string --
|
|
16
|
+
typically a closure around an LLM provider's client call. This makes
|
|
17
|
+
mask_and_call provider-agnostic: it works with Claude, OpenAI, Gemini, a
|
|
18
|
+
local model, or anything else, without MaskFlow depending on any
|
|
19
|
+
provider's SDK.
|
|
20
|
+
"""
|
|
21
|
+
result = mask(prompt, min_confidence=min_confidence)
|
|
22
|
+
response = call_fn(result.masked_text)
|
|
23
|
+
return unmask(response, result.mapping)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from maskflow import Finding, MaskResult, PIIType, detect, mask, mask_and_call, unmask
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_mask_and_call_never_exposes_pii_to_call_fn():
|
|
5
|
+
seen = {}
|
|
6
|
+
|
|
7
|
+
def call_fn(masked_prompt: str) -> str:
|
|
8
|
+
seen["prompt"] = masked_prompt
|
|
9
|
+
return f"Sure, I'll email you back at {masked_prompt.split('at ')[1]}"
|
|
10
|
+
|
|
11
|
+
prompt = "Email me at alice@example.com with the update."
|
|
12
|
+
mask_and_call(prompt, call_fn)
|
|
13
|
+
|
|
14
|
+
assert "alice@example.com" not in seen["prompt"]
|
|
15
|
+
assert "<EMAIL_1>" in seen["prompt"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_mask_and_call_restores_original_values_in_response():
|
|
19
|
+
def call_fn(masked_prompt: str) -> str:
|
|
20
|
+
return f"Got it, confirming: {masked_prompt}"
|
|
21
|
+
|
|
22
|
+
prompt = "My phone number is 415-555-0132."
|
|
23
|
+
result = mask_and_call(prompt, call_fn)
|
|
24
|
+
|
|
25
|
+
assert "415-555-0132" in result
|
|
26
|
+
assert "<PHONE_1>" not in result
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_mask_and_call_is_provider_agnostic():
|
|
30
|
+
"""call_fn is just a plain callable -- swapping providers means swapping the
|
|
31
|
+
closure, not touching mask_and_call itself."""
|
|
32
|
+
|
|
33
|
+
def fake_anthropic_call(masked_prompt: str) -> str:
|
|
34
|
+
return f"[claude] {masked_prompt}"
|
|
35
|
+
|
|
36
|
+
def fake_openai_call(masked_prompt: str) -> str:
|
|
37
|
+
return f"[gpt] {masked_prompt}"
|
|
38
|
+
|
|
39
|
+
prompt = "Contact bob@example.com."
|
|
40
|
+
assert mask_and_call(prompt, fake_anthropic_call) == f"[claude] {prompt}"
|
|
41
|
+
assert mask_and_call(prompt, fake_openai_call) == f"[gpt] {prompt}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_sdk_reexports_core_primitives():
|
|
45
|
+
text = "Reach me at carol@example.com."
|
|
46
|
+
result = mask(text)
|
|
47
|
+
|
|
48
|
+
assert isinstance(result, MaskResult)
|
|
49
|
+
assert unmask(result.masked_text, result.mapping) == text
|
|
50
|
+
assert detect(text)[0].type == PIIType.EMAIL
|
|
51
|
+
assert isinstance(detect(text)[0], Finding)
|