maskflow-langchain 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_langchain-0.1.0/.gitignore +16 -0
- maskflow_langchain-0.1.0/PKG-INFO +122 -0
- maskflow_langchain-0.1.0/README.md +105 -0
- maskflow_langchain-0.1.0/examples/README.md +22 -0
- maskflow_langchain-0.1.0/examples/anonymized_chain.py +57 -0
- maskflow_langchain-0.1.0/pyproject.toml +48 -0
- maskflow_langchain-0.1.0/src/maskflow_langchain/__init__.py +47 -0
- maskflow_langchain-0.1.0/src/maskflow_langchain/_mapping.py +52 -0
- maskflow_langchain-0.1.0/src/maskflow_langchain/anonymizer.py +335 -0
- maskflow_langchain-0.1.0/src/maskflow_langchain/base.py +59 -0
- maskflow_langchain-0.1.0/src/maskflow_langchain/callbacks.py +189 -0
- maskflow_langchain-0.1.0/src/maskflow_langchain/matching.py +42 -0
- maskflow_langchain-0.1.0/src/maskflow_langchain/py.typed +0 -0
- maskflow_langchain-0.1.0/src/maskflow_langchain/runnables.py +103 -0
- maskflow_langchain-0.1.0/tests/conftest.py +3 -0
- maskflow_langchain-0.1.0/tests/test_langchain_anonymizer.py +115 -0
- maskflow_langchain-0.1.0/tests/test_langchain_callbacks.py +73 -0
- maskflow_langchain-0.1.0/tests/test_langchain_no_pii_leak.py +61 -0
- maskflow_langchain-0.1.0/tests/test_langchain_persistence_and_parity.py +75 -0
- maskflow_langchain-0.1.0/tests/test_langchain_runnables.py +83 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: maskflow-langchain
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MaskFlow for LangChain: a reversible PII anonymizer/deanonymizer pair (drop-in for langchain-experimental's Presidio anonymizer) plus a leak-guard callback. Indian identifiers included.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: langchain-core<2,>=0.3
|
|
8
|
+
Requires-Dist: maskflow-sdk<0.9,>=0.8.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: hypothesis>=6.100; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: pyyaml>=6.0; extra == 'dev'
|
|
14
|
+
Provides-Extra: yaml
|
|
15
|
+
Requires-Dist: pyyaml>=6.0; extra == 'yaml'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# maskflow-langchain
|
|
19
|
+
|
|
20
|
+
MaskFlow for [LangChain](https://github.com/langchain-ai/langchain): a
|
|
21
|
+
reversible PII anonymizer / deanonymizer pair that drops in for
|
|
22
|
+
`langchain-experimental`'s Presidio anonymizer, plus a leak-guard callback.
|
|
23
|
+
|
|
24
|
+
It runs MaskFlow's detection engine, so alongside the usual PII (email,
|
|
25
|
+
phone, card numbers, ...) it covers the **Indian identifiers** most tools
|
|
26
|
+
miss: Aadhaar, PAN, GSTIN, UPI VPA, IFSC, ABHA, Indian mobile / PIN code /
|
|
27
|
+
voter ID / passport / driving licence / vehicle registration, and Indian
|
|
28
|
+
names and addresses.
|
|
29
|
+
|
|
30
|
+
- **Drop-in.** Same method names and mapping shapes as
|
|
31
|
+
`PresidioReversibleAnonymizer`, so migrating a chain is one import line.
|
|
32
|
+
- **Streaming.** `anonymizer.deanonymizer` is a streaming-aware `Runnable`;
|
|
33
|
+
a placeholder split across two streamed chunks is stitched back before
|
|
34
|
+
the caller sees it. (Presidio's `RunnableLambda(deanonymize)` only fires
|
|
35
|
+
on the final string.)
|
|
36
|
+
- **Leak guard.** An optional callback that fails a call closed if a prompt
|
|
37
|
+
still contains PII.
|
|
38
|
+
- **MIT, no gates, no telemetry.**
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install maskflow-langchain
|
|
44
|
+
pip install "maskflow-langchain[yaml]" # if you save/load mappings as .yaml
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`langchain-core` is a real dependency (`>=0.3,<2`). The first detection run
|
|
48
|
+
downloads a small spaCy model for the name/address recognizers; pass
|
|
49
|
+
`patterns_only=True` to skip it.
|
|
50
|
+
|
|
51
|
+
## Migrating from the Presidio anonymizer
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# from langchain_experimental.data_anonymizer import PresidioReversibleAnonymizer
|
|
55
|
+
from maskflow_langchain import MaskflowReversibleAnonymizer as PresidioReversibleAnonymizer
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Everything a chain touches keeps working: `.anonymize(text, language=None,
|
|
59
|
+
allow_list=None)`, `.deanonymize(text, strategy=exact_matching_strategy)`,
|
|
60
|
+
`.reset_deanonymizer_mapping()`, `.deanonymizer_mapping`,
|
|
61
|
+
`.anonymizer_mapping`, `.save_deanonymizer_mapping(path)`,
|
|
62
|
+
`.load_deanonymizer_mapping(path)`.
|
|
63
|
+
|
|
64
|
+
Two methods differ, because Presidio recognizer and operator objects have
|
|
65
|
+
no MaskFlow equivalent:
|
|
66
|
+
|
|
67
|
+
| Presidio | maskflow-langchain |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `add_recognizer(recognizer_obj)` | `add_recognizer(entity_type=..., regex=..., base_confidence=0.6)` |
|
|
70
|
+
| `add_operators({e: OperatorConfig(...)})` | `add_operators({e: "replace"\|"redact"\|"mask"\|"hash"\|"surrogate"})` |
|
|
71
|
+
|
|
72
|
+
`allow_list` is passed to the **constructor** on the reversible anonymizer
|
|
73
|
+
(the session is built once); a differing per-call `allow_list` raises.
|
|
74
|
+
|
|
75
|
+
## Use it in a chain
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from langchain_core.output_parsers import StrOutputParser
|
|
79
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
80
|
+
from maskflow_langchain import MaskflowReversibleAnonymizer
|
|
81
|
+
|
|
82
|
+
anonymizer = MaskflowReversibleAnonymizer()
|
|
83
|
+
prompt = ChatPromptTemplate.from_template("Answer: {question}")
|
|
84
|
+
|
|
85
|
+
chain = (
|
|
86
|
+
{"question": lambda x: anonymizer.anonymize(x["question"])}
|
|
87
|
+
| prompt
|
|
88
|
+
| llm
|
|
89
|
+
| StrOutputParser()
|
|
90
|
+
| anonymizer.deanonymizer # streaming-aware
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
chain.invoke({"question": "Is PAN ABCPE1234F valid for a salaried filer?"})
|
|
94
|
+
# the LLM sees "<PAN_1>"; you get "ABCPE1234F" back
|
|
95
|
+
for piece in chain.stream({"question": "Confirm receipt of PAN ABCPE1234F"}):
|
|
96
|
+
print(piece, end="") # deanonymized incrementally
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Leak-guard callback
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from maskflow_langchain import MaskflowLeakGuardCallback
|
|
103
|
+
|
|
104
|
+
guard = MaskflowLeakGuardCallback(raise_on_prompt_pii=True)
|
|
105
|
+
chain.invoke(x, config={"callbacks": [guard]})
|
|
106
|
+
# raises MaskflowPIILeakError if a prompt reaching the LLM still has PII
|
|
107
|
+
|
|
108
|
+
guard.summary() # {"prompt": {"PAN": 0}, "completion": {...}} -- counts only, never values
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Callbacks cannot rewrite prompts, so this does not mask; it audits (entity
|
|
112
|
+
types and counts, never values) and, with `raise_on_prompt_pii=True`, aborts
|
|
113
|
+
a call that would leak.
|
|
114
|
+
|
|
115
|
+
## PII safety
|
|
116
|
+
|
|
117
|
+
No original value is written to logs, `repr`, callback state, or a saved
|
|
118
|
+
mapping's structure beyond what you explicitly persist with
|
|
119
|
+
`save_deanonymizer_mapping` (which, like Presidio's, contains the real
|
|
120
|
+
values -- treat that file as sensitive).
|
|
121
|
+
|
|
122
|
+
See `docs/langchain.md` in the MaskFlow repo for design notes.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# maskflow-langchain
|
|
2
|
+
|
|
3
|
+
MaskFlow for [LangChain](https://github.com/langchain-ai/langchain): a
|
|
4
|
+
reversible PII anonymizer / deanonymizer pair that drops in for
|
|
5
|
+
`langchain-experimental`'s Presidio anonymizer, plus a leak-guard callback.
|
|
6
|
+
|
|
7
|
+
It runs MaskFlow's detection engine, so alongside the usual PII (email,
|
|
8
|
+
phone, card numbers, ...) it covers the **Indian identifiers** most tools
|
|
9
|
+
miss: Aadhaar, PAN, GSTIN, UPI VPA, IFSC, ABHA, Indian mobile / PIN code /
|
|
10
|
+
voter ID / passport / driving licence / vehicle registration, and Indian
|
|
11
|
+
names and addresses.
|
|
12
|
+
|
|
13
|
+
- **Drop-in.** Same method names and mapping shapes as
|
|
14
|
+
`PresidioReversibleAnonymizer`, so migrating a chain is one import line.
|
|
15
|
+
- **Streaming.** `anonymizer.deanonymizer` is a streaming-aware `Runnable`;
|
|
16
|
+
a placeholder split across two streamed chunks is stitched back before
|
|
17
|
+
the caller sees it. (Presidio's `RunnableLambda(deanonymize)` only fires
|
|
18
|
+
on the final string.)
|
|
19
|
+
- **Leak guard.** An optional callback that fails a call closed if a prompt
|
|
20
|
+
still contains PII.
|
|
21
|
+
- **MIT, no gates, no telemetry.**
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install maskflow-langchain
|
|
27
|
+
pip install "maskflow-langchain[yaml]" # if you save/load mappings as .yaml
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`langchain-core` is a real dependency (`>=0.3,<2`). The first detection run
|
|
31
|
+
downloads a small spaCy model for the name/address recognizers; pass
|
|
32
|
+
`patterns_only=True` to skip it.
|
|
33
|
+
|
|
34
|
+
## Migrating from the Presidio anonymizer
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
# from langchain_experimental.data_anonymizer import PresidioReversibleAnonymizer
|
|
38
|
+
from maskflow_langchain import MaskflowReversibleAnonymizer as PresidioReversibleAnonymizer
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Everything a chain touches keeps working: `.anonymize(text, language=None,
|
|
42
|
+
allow_list=None)`, `.deanonymize(text, strategy=exact_matching_strategy)`,
|
|
43
|
+
`.reset_deanonymizer_mapping()`, `.deanonymizer_mapping`,
|
|
44
|
+
`.anonymizer_mapping`, `.save_deanonymizer_mapping(path)`,
|
|
45
|
+
`.load_deanonymizer_mapping(path)`.
|
|
46
|
+
|
|
47
|
+
Two methods differ, because Presidio recognizer and operator objects have
|
|
48
|
+
no MaskFlow equivalent:
|
|
49
|
+
|
|
50
|
+
| Presidio | maskflow-langchain |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `add_recognizer(recognizer_obj)` | `add_recognizer(entity_type=..., regex=..., base_confidence=0.6)` |
|
|
53
|
+
| `add_operators({e: OperatorConfig(...)})` | `add_operators({e: "replace"\|"redact"\|"mask"\|"hash"\|"surrogate"})` |
|
|
54
|
+
|
|
55
|
+
`allow_list` is passed to the **constructor** on the reversible anonymizer
|
|
56
|
+
(the session is built once); a differing per-call `allow_list` raises.
|
|
57
|
+
|
|
58
|
+
## Use it in a chain
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from langchain_core.output_parsers import StrOutputParser
|
|
62
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
63
|
+
from maskflow_langchain import MaskflowReversibleAnonymizer
|
|
64
|
+
|
|
65
|
+
anonymizer = MaskflowReversibleAnonymizer()
|
|
66
|
+
prompt = ChatPromptTemplate.from_template("Answer: {question}")
|
|
67
|
+
|
|
68
|
+
chain = (
|
|
69
|
+
{"question": lambda x: anonymizer.anonymize(x["question"])}
|
|
70
|
+
| prompt
|
|
71
|
+
| llm
|
|
72
|
+
| StrOutputParser()
|
|
73
|
+
| anonymizer.deanonymizer # streaming-aware
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
chain.invoke({"question": "Is PAN ABCPE1234F valid for a salaried filer?"})
|
|
77
|
+
# the LLM sees "<PAN_1>"; you get "ABCPE1234F" back
|
|
78
|
+
for piece in chain.stream({"question": "Confirm receipt of PAN ABCPE1234F"}):
|
|
79
|
+
print(piece, end="") # deanonymized incrementally
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Leak-guard callback
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from maskflow_langchain import MaskflowLeakGuardCallback
|
|
86
|
+
|
|
87
|
+
guard = MaskflowLeakGuardCallback(raise_on_prompt_pii=True)
|
|
88
|
+
chain.invoke(x, config={"callbacks": [guard]})
|
|
89
|
+
# raises MaskflowPIILeakError if a prompt reaching the LLM still has PII
|
|
90
|
+
|
|
91
|
+
guard.summary() # {"prompt": {"PAN": 0}, "completion": {...}} -- counts only, never values
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Callbacks cannot rewrite prompts, so this does not mask; it audits (entity
|
|
95
|
+
types and counts, never values) and, with `raise_on_prompt_pii=True`, aborts
|
|
96
|
+
a call that would leak.
|
|
97
|
+
|
|
98
|
+
## PII safety
|
|
99
|
+
|
|
100
|
+
No original value is written to logs, `repr`, callback state, or a saved
|
|
101
|
+
mapping's structure beyond what you explicitly persist with
|
|
102
|
+
`save_deanonymizer_mapping` (which, like Presidio's, contains the real
|
|
103
|
+
values -- treat that file as sensitive).
|
|
104
|
+
|
|
105
|
+
See `docs/langchain.md` in the MaskFlow repo for design notes.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Runnable example
|
|
2
|
+
|
|
3
|
+
`anonymized_chain.py` builds an LCEL chain that masks PII before the model
|
|
4
|
+
and restores it after, and attaches the leak-guard callback.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install maskflow-langchain
|
|
8
|
+
python packages/maskflow-langchain/examples/anonymized_chain.py
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
With `OPENAI_API_KEY` set it calls `gpt-4o-mini`; without one it uses a fake
|
|
12
|
+
chat model so the mask/restore round-trip is still visible:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
you asked : Please file the return for PAN ABCPE1234F, UPI ramesh@oksbi, email ramesh@example.com.
|
|
16
|
+
model saw : Please file the return for PAN <PAN_1>, UPI <UPI_VPA_1>, email <EMAIL_1>.
|
|
17
|
+
you get back: ...
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`chain.stream(...)` deanonymizes incrementally through
|
|
21
|
+
`anonymizer.deanonymizer`; `guard.summary()` reports detected entity types
|
|
22
|
+
and counts, never values.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Runnable example: a LangChain LCEL chain with MaskFlow anonymization.
|
|
2
|
+
|
|
3
|
+
pip install maskflow-langchain "langchain[openai]"
|
|
4
|
+
export OPENAI_API_KEY=sk-...
|
|
5
|
+
python packages/maskflow-langchain/examples/anonymized_chain.py
|
|
6
|
+
|
|
7
|
+
Swap the model line for any chat model. With no API key it falls back to a
|
|
8
|
+
fake model that echoes the (masked) prompt, so the round-trip is still
|
|
9
|
+
visible.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
from langchain_core.output_parsers import StrOutputParser
|
|
17
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
18
|
+
from maskflow_langchain import MaskflowLeakGuardCallback, MaskflowReversibleAnonymizer
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _model(): # noqa: ANN202
|
|
22
|
+
if os.getenv("OPENAI_API_KEY"):
|
|
23
|
+
from langchain_openai import ChatOpenAI
|
|
24
|
+
|
|
25
|
+
return ChatOpenAI(model="gpt-4o-mini", temperature=0)
|
|
26
|
+
from langchain_core.language_models.fake_chat_models import FakeMessagesListChatModel
|
|
27
|
+
from langchain_core.messages import AIMessage
|
|
28
|
+
|
|
29
|
+
return FakeMessagesListChatModel(
|
|
30
|
+
responses=[AIMessage("Noted. I will reference the details you provided.")]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
anonymizer = MaskflowReversibleAnonymizer()
|
|
35
|
+
prompt = ChatPromptTemplate.from_template(
|
|
36
|
+
"A user wrote: {question}\nAcknowledge and restate the identifiers you saw."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
chain = (
|
|
40
|
+
{"question": lambda x: anonymizer.anonymize(x["question"])}
|
|
41
|
+
| prompt
|
|
42
|
+
| _model()
|
|
43
|
+
| StrOutputParser()
|
|
44
|
+
| anonymizer.deanonymizer
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
question = "Please file the return for PAN ABCPE1234F, UPI ramesh@oksbi, email ramesh@example.com."
|
|
48
|
+
|
|
49
|
+
guard = MaskflowLeakGuardCallback()
|
|
50
|
+
print("you asked :", question)
|
|
51
|
+
print("model saw :", anonymizer.anonymize(question))
|
|
52
|
+
print("you get back:", chain.invoke({"question": question}, config={"callbacks": [guard]}))
|
|
53
|
+
print("streamed :", end=" ")
|
|
54
|
+
for piece in chain.stream({"question": question}):
|
|
55
|
+
print(piece, end="", flush=True)
|
|
56
|
+
print()
|
|
57
|
+
print("audit :", guard.summary())
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "maskflow-langchain"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MaskFlow for LangChain: a reversible PII anonymizer/deanonymizer pair (drop-in for langchain-experimental's Presidio anonymizer) plus a leak-guard callback. Indian identifiers included."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
dependencies = [
|
|
9
|
+
# The masking engine, session identity (stable token <-> value), and the
|
|
10
|
+
# fuzz-tested StreamingUnmasker (0.8.0 -> maskflow.streaming) that the
|
|
11
|
+
# streaming deanonymizer is built on.
|
|
12
|
+
"maskflow-sdk>=0.8.0,<0.9",
|
|
13
|
+
# BaseCallbackHandler + Runnable. Pure-python, small; a real dependency,
|
|
14
|
+
# not a peer -- you install maskflow-langchain to use it *with* LangChain.
|
|
15
|
+
# Range spans the 0.3 line (still widely deployed) and 1.x.
|
|
16
|
+
"langchain-core>=0.3,<2",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
# save_deanonymizer_mapping / load_deanonymizer_mapping with .yaml paths.
|
|
21
|
+
yaml = [
|
|
22
|
+
"pyyaml>=6.0",
|
|
23
|
+
]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=8.0",
|
|
26
|
+
"pytest-asyncio>=0.23",
|
|
27
|
+
"hypothesis>=6.100",
|
|
28
|
+
"pyyaml>=6.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[tool.uv.sources]
|
|
32
|
+
maskflow-sdk = { workspace = true }
|
|
33
|
+
|
|
34
|
+
[tool.uv]
|
|
35
|
+
package = true
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["hatchling"]
|
|
39
|
+
build-backend = "hatchling.build"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["src/maskflow_langchain"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
asyncio_mode = "auto"
|
|
46
|
+
markers = [
|
|
47
|
+
"leak: guards against PII reaching logs, repr, callback state, or saved mappings",
|
|
48
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""MaskFlow for LangChain.
|
|
2
|
+
|
|
3
|
+
A reversible PII anonymizer / deanonymizer pair that drops in for
|
|
4
|
+
``langchain_experimental.data_anonymizer``'s Presidio anonymizer (Indian
|
|
5
|
+
identifiers included), plus a leak-guard callback.
|
|
6
|
+
|
|
7
|
+
from maskflow_langchain import MaskflowReversibleAnonymizer
|
|
8
|
+
|
|
9
|
+
anonymizer = MaskflowReversibleAnonymizer()
|
|
10
|
+
chain = (
|
|
11
|
+
{"question": lambda x: anonymizer.anonymize(x["question"])}
|
|
12
|
+
| prompt
|
|
13
|
+
| llm
|
|
14
|
+
| StrOutputParser()
|
|
15
|
+
| anonymizer.deanonymizer # streaming-aware Runnable
|
|
16
|
+
)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from .anonymizer import MaskflowAnonymizer, MaskflowReversibleAnonymizer
|
|
22
|
+
from .base import AnonymizerBase, ReversibleAnonymizerBase
|
|
23
|
+
from .callbacks import (
|
|
24
|
+
AsyncMaskflowLeakGuardCallback,
|
|
25
|
+
MaskflowLeakGuardCallback,
|
|
26
|
+
MaskflowPIILeakError,
|
|
27
|
+
)
|
|
28
|
+
from .matching import (
|
|
29
|
+
MappingDataType,
|
|
30
|
+
case_insensitive_matching_strategy,
|
|
31
|
+
exact_matching_strategy,
|
|
32
|
+
)
|
|
33
|
+
from .runnables import MaskflowDeanonymizer
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"MaskflowAnonymizer",
|
|
37
|
+
"MaskflowReversibleAnonymizer",
|
|
38
|
+
"MaskflowDeanonymizer",
|
|
39
|
+
"MaskflowLeakGuardCallback",
|
|
40
|
+
"AsyncMaskflowLeakGuardCallback",
|
|
41
|
+
"MaskflowPIILeakError",
|
|
42
|
+
"AnonymizerBase",
|
|
43
|
+
"ReversibleAnonymizerBase",
|
|
44
|
+
"MappingDataType",
|
|
45
|
+
"exact_matching_strategy",
|
|
46
|
+
"case_insensitive_matching_strategy",
|
|
47
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Conversions between a ``maskflow.Session`` mapping and LangChain's
|
|
2
|
+
``MappingDataType`` (``{entity_type: {anonymized: original}}``).
|
|
3
|
+
|
|
4
|
+
The nested-by-entity-type shape is what ``PresidioReversibleAnonymizer``
|
|
5
|
+
exposes as ``.deanonymizer_mapping`` / ``.anonymizer_mapping`` and what its
|
|
6
|
+
``save``/``load`` round-trips, so a chain that inspects or persists the
|
|
7
|
+
mapping keeps working after the import swap.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from maskflow import Session
|
|
13
|
+
|
|
14
|
+
from .matching import MappingDataType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def session_deanonymizer_mapping(session: Session) -> MappingDataType:
|
|
18
|
+
"""``{ENTITY: {<token>: original}}`` for every reversible entry in the
|
|
19
|
+
session's current mapping."""
|
|
20
|
+
out: MappingDataType = {}
|
|
21
|
+
mapping = session.mapping
|
|
22
|
+
for token in mapping:
|
|
23
|
+
entry = mapping[token]
|
|
24
|
+
if not entry.reversible:
|
|
25
|
+
continue
|
|
26
|
+
out.setdefault(entry.entity_type.value, {})[token] = entry.original
|
|
27
|
+
return out
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def invert(mapping_data: MappingDataType) -> MappingDataType:
|
|
31
|
+
"""``{ENTITY: {token: original}}`` -> ``{ENTITY: {original: token}}``."""
|
|
32
|
+
return {
|
|
33
|
+
entity_type: {original: anon for anon, original in inner.items()}
|
|
34
|
+
for entity_type, inner in mapping_data.items()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def flat_token_pairs(mapping_data: MappingDataType) -> dict[str, str]:
|
|
39
|
+
"""Flatten to ``{<token>: original}`` for whole-text or streaming unmask."""
|
|
40
|
+
pairs: dict[str, str] = {}
|
|
41
|
+
for inner in mapping_data.values():
|
|
42
|
+
pairs.update(inner)
|
|
43
|
+
return pairs
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def merge_into(dst: MappingDataType, src: MappingDataType) -> None:
|
|
47
|
+
"""Merge ``src`` into ``dst`` in place; existing token->original entries
|
|
48
|
+
are not overwritten."""
|
|
49
|
+
for entity_type, inner in src.items():
|
|
50
|
+
target = dst.setdefault(entity_type, {})
|
|
51
|
+
for anon, original in inner.items():
|
|
52
|
+
target.setdefault(anon, original)
|