Protect personal data (PII) in your LLM prompts. piighost hides sensitive values from the model, then restores the real values in the response, so tools and the user still get the real data. Pluggable detectors (regex, NER, LLM), LangChain, Pydantic and LlamaIndex AI integrations, and a companion OpenAI/Anthropic proxy.
13
stars
883
commits
Python
primary language
Sep 10, 2026
updated
piighost is a Python library that protects your personal data (PII) in conversations with LLMs through de-identification. Sensitive values are hidden before they are sent, then restored in the response. LangChain, Pydantic AI, LlamaIndex and Claude Code integrations are provided, together with an OpenAI and Anthropic API connector.
This de-identification spots PII with pluggable detectors (regex, NER, LLM) and replaces each value with a placeholder, the token that takes its place. For example:
John Doe becomes <<PERSON:1>>john.doe@example.com becomes <<EMAIL:1>>This placeholder stays the same from one message to the next with the conversational pipeline, which keeps the mapping between a value and its placeholder across the whole conversation. If john.doe@example.com reappears three messages later, the placeholder is still <<EMAIL:1>>, which lets the LLM follow the thread.
The LLM therefore only receives de-identified text. When it returns placeholders, for example by answering Hello <<PERSON:1>>, piighost replaces them with the real values. The user sees John Doe and never sees the de-identification.
The same mechanism protects agents that call tools. With the LangChain middleware, a tool that needs the real email address receives it in clear, while the LLM that supplies it only writes <<EMAIL:1>>.
The LLM only sees placeholders. The tool receives the real address, the user gets a clear-text reply, and your agent code stays the same.
[!NOTE] This retained mapping makes the de-identification a pseudonymization under the GDPR, not a definitive anonymization. With the conversational pipeline, the real values stay stored for the duration of the conversation and must be protected accordingly.
Most PII tooling stops at detection. Presidio, GLiNER, spaCy, and regex catalogs all find entities in text, and they do it well. The hard part for an LLM agent is everything after detection: swapping values without wrecking the model's reasoning, keeping one value mapped to one token across a conversation, handing tools the real value while the model sees only the token, and putting the originals back in the reply. That orchestration is what piighost is.
What piighost adds on top:
<<PERSON:1>> and is put back automatically, so the end user reads john.doe@example.com and never sees a token. Label-only, masked, and keyed-hash factories are available too.py.typed and a minimal core with everything heavy behind extras, plus OpenTelemetry per-stage spans (viewable in Langfuse or Jaeger) with optional payload redaction.piighost protects a running conversation message by message, not a static dataset.For how it stacks up against Presidio, LangChain, the cloud APIs, and others, see How PIIGhost compares.
piighost uses an id (<<PERSON:1>>) backed by a cache. The reason: a token that carries the ciphertext can be captured today and cracked in 20 years ("harvest now, decrypt later", the quantum threat to classical crypto), whereas an id reveals nothing on its own. In return, you need a cache to hold the token-to-value mapping, so a memory backend to deploy, share across workers, and persist in production.piighost protects live text and conversations, not a whole dataset. For that, see ARX, Amnesia, or Google DLP.RegexDetector matches on shape alone so it never lets a real value mangled by OCR leak (a checksum would reject it and it would pass in clear). In exchange, it sometimes flags a string that only looks like PII, which costs nothing beyond one extra token.pip install piighost # or: uv add piighost
ExactMatchDetector de-identifies a dictionary of known values without downloading a model.
import asyncio
from piighost.components.detector import ExactMatchDetector
from piighost.pipeline import AnonymizationPipeline
detector = ExactMatchDetector({"John Doe": "PERSON", "john.doe@example.com": "EMAIL"})
pipeline = AnonymizationPipeline(detector)
result = asyncio.run(pipeline.anonymize("Write to John Doe at john.doe@example.com."))
print(result.text) # Write to <<PERSON:1>> at <<EMAIL:1>>.
The middleware wraps a conversational pipeline and handles every agent turn for you, so the same de-identification applies without any change to your agent logic.
pip install 'piighost[langchain]' # or: uv add 'piighost[langchain]'
import asyncio
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from piighost.components.detector import ExactMatchDetector
from piighost.integrations.langchain import PIIAnonymizationMiddleware
from piighost.pipeline import ThreadAnonymizationPipeline
SYSTEM_PROMPT = (
"Some inputs contain placeholders like <<PERSON:1>> that stand in for real "
"values withheld for privacy. Treat each placeholder as the real value, never "
"comment on its format, and pass it to tools unchanged."
)
@tool
def send_mail(to: str, body: str) -> str:
"""Send an email to `to` with the given body."""
print(f"[tool] send_mail received to={to!r}")
return "Email successfully sent."
async def main() -> None:
# This example calls OpenAI, so set OPENAI_API_KEY in your environment first.
labels = {"Patrick Dupont": "PERSON", "patrick@acme.com": "EMAIL"}
detector = ExactMatchDetector(labels)
pipeline = ThreadAnonymizationPipeline(detector)
middleware = PIIAnonymizationMiddleware(pipeline)
# gpt-5.6-terra is a reasoning model; reasoning_effort="none" lets it call
# function tools over chat/completions.
model = init_chat_model("openai:gpt-5.6-terra", reasoning_effort="none")
# The system prompt tells the model to treat placeholders as real values and
# pass them to tools unchanged, so it does not balk at the tokens.
agent = create_agent(
model=model,
system_prompt=SYSTEM_PROMPT,
tools=[send_mail],
middleware=[middleware],
)
config = {"configurable": {"thread_id": "demo-thread"}}
message = HumanMessage(
"Use the send_mail tool to send a welcome note to Patrick Dupont at patrick@acme.com."
)
result = await agent.ainvoke({"messages": [message]}, config=config)
print(f"user sees: {result['messages'][-1].content!r}")
if __name__ == "__main__":
asyncio.run(main())
This is the LangChain integration, but it is only one option. piighost also has connectors for Pydantic AI and LlamaIndex, and the companion piighost-api exposes OpenAI- and Anthropic-compatible proxies, so you can move de-identification to the HTTP boundary with only a base URL change.
For a real detector and the conversational pipeline, see the Quickstart and the LangChain integration.
875 commits
8 commits
Python
99.8%
Protect personal data (PII) in your LLM prompts. piighost hides sensitive values from the model, then restores the real values in the response, so tools and the user still get the real data. Pluggable detectors (regex, NER, LLM), LangChain, Pydantic and LlamaIndex AI integrations, and a companion OpenAI/Anthropic proxy.
13
stars
883
commits
Python
primary language
Sep 10, 2026
updated
piighost is a Python library that protects your personal data (PII) in conversations with LLMs through de-identification. Sensitive values are hidden before they are sent, then restored in the response. LangChain, Pydantic AI, LlamaIndex and Claude Code integrations are provided, together with an OpenAI and Anthropic API connector.
This de-identification spots PII with pluggable detectors (regex, NER, LLM) and replaces each value with a placeholder, the token that takes its place. For example:
John Doe becomes <<PERSON:1>>john.doe@example.com becomes <<EMAIL:1>>This placeholder stays the same from one message to the next with the conversational pipeline, which keeps the mapping between a value and its placeholder across the whole conversation. If john.doe@example.com reappears three messages later, the placeholder is still <<EMAIL:1>>, which lets the LLM follow the thread.
The LLM therefore only receives de-identified text. When it returns placeholders, for example by answering Hello <<PERSON:1>>, piighost replaces them with the real values. The user sees John Doe and never sees the de-identification.
The same mechanism protects agents that call tools. With the LangChain middleware, a tool that needs the real email address receives it in clear, while the LLM that supplies it only writes <<EMAIL:1>>.
The LLM only sees placeholders. The tool receives the real address, the user gets a clear-text reply, and your agent code stays the same.
[!NOTE] This retained mapping makes the de-identification a pseudonymization under the GDPR, not a definitive anonymization. With the conversational pipeline, the real values stay stored for the duration of the conversation and must be protected accordingly.
Most PII tooling stops at detection. Presidio, GLiNER, spaCy, and regex catalogs all find entities in text, and they do it well. The hard part for an LLM agent is everything after detection: swapping values without wrecking the model's reasoning, keeping one value mapped to one token across a conversation, handing tools the real value while the model sees only the token, and putting the originals back in the reply. That orchestration is what piighost is.
What piighost adds on top:
<<PERSON:1>> and is put back automatically, so the end user reads john.doe@example.com and never sees a token. Label-only, masked, and keyed-hash factories are available too.py.typed and a minimal core with everything heavy behind extras, plus OpenTelemetry per-stage spans (viewable in Langfuse or Jaeger) with optional payload redaction.piighost protects a running conversation message by message, not a static dataset.For how it stacks up against Presidio, LangChain, the cloud APIs, and others, see How PIIGhost compares.
piighost uses an id (<<PERSON:1>>) backed by a cache. The reason: a token that carries the ciphertext can be captured today and cracked in 20 years ("harvest now, decrypt later", the quantum threat to classical crypto), whereas an id reveals nothing on its own. In return, you need a cache to hold the token-to-value mapping, so a memory backend to deploy, share across workers, and persist in production.piighost protects live text and conversations, not a whole dataset. For that, see ARX, Amnesia, or Google DLP.RegexDetector matches on shape alone so it never lets a real value mangled by OCR leak (a checksum would reject it and it would pass in clear). In exchange, it sometimes flags a string that only looks like PII, which costs nothing beyond one extra token.pip install piighost # or: uv add piighost
ExactMatchDetector de-identifies a dictionary of known values without downloading a model.
import asyncio
from piighost.components.detector import ExactMatchDetector
from piighost.pipeline import AnonymizationPipeline
detector = ExactMatchDetector({"John Doe": "PERSON", "john.doe@example.com": "EMAIL"})
pipeline = AnonymizationPipeline(detector)
result = asyncio.run(pipeline.anonymize("Write to John Doe at john.doe@example.com."))
print(result.text) # Write to <<PERSON:1>> at <<EMAIL:1>>.
The middleware wraps a conversational pipeline and handles every agent turn for you, so the same de-identification applies without any change to your agent logic.
pip install 'piighost[langchain]' # or: uv add 'piighost[langchain]'
import asyncio
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from piighost.components.detector import ExactMatchDetector
from piighost.integrations.langchain import PIIAnonymizationMiddleware
from piighost.pipeline import ThreadAnonymizationPipeline
SYSTEM_PROMPT = (
"Some inputs contain placeholders like <<PERSON:1>> that stand in for real "
"values withheld for privacy. Treat each placeholder as the real value, never "
"comment on its format, and pass it to tools unchanged."
)
@tool
def send_mail(to: str, body: str) -> str:
"""Send an email to `to` with the given body."""
print(f"[tool] send_mail received to={to!r}")
return "Email successfully sent."
async def main() -> None:
# This example calls OpenAI, so set OPENAI_API_KEY in your environment first.
labels = {"Patrick Dupont": "PERSON", "patrick@acme.com": "EMAIL"}
detector = ExactMatchDetector(labels)
pipeline = ThreadAnonymizationPipeline(detector)
middleware = PIIAnonymizationMiddleware(pipeline)
# gpt-5.6-terra is a reasoning model; reasoning_effort="none" lets it call
# function tools over chat/completions.
model = init_chat_model("openai:gpt-5.6-terra", reasoning_effort="none")
# The system prompt tells the model to treat placeholders as real values and
# pass them to tools unchanged, so it does not balk at the tokens.
agent = create_agent(
model=model,
system_prompt=SYSTEM_PROMPT,
tools=[send_mail],
middleware=[middleware],
)
config = {"configurable": {"thread_id": "demo-thread"}}
message = HumanMessage(
"Use the send_mail tool to send a welcome note to Patrick Dupont at patrick@acme.com."
)
result = await agent.ainvoke({"messages": [message]}, config=config)
print(f"user sees: {result['messages'][-1].content!r}")
if __name__ == "__main__":
asyncio.run(main())
This is the LangChain integration, but it is only one option. piighost also has connectors for Pydantic AI and LlamaIndex, and the companion piighost-api exposes OpenAI- and Anthropic-compatible proxies, so you can move de-identification to the HTTP boundary with only a base URL change.
For a real detector and the conversational pipeline, see the Quickstart and the LangChain integration.
875 commits
8 commits
Python
99.8%