Skip to content
SummerSeaSunPublic

About

A local privacy layer that detects and pseudonymizes sensitive data before sending prompts to LLMs.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

PuliAI

Local PII detection and pseudonymization for LLM prompts.

PuliAI detects sensitive data in text and replaces it with stable placeholders before the content is sent to an LLM.

The original values remain local.

License: GPL v3

PuliAI demo

Copyright (C) 2026 SummerSeaSun

AI was used to generate code in the project, while the author defined its style, logic, and direction.


Example

Input:

Please contact Mario Rossi at mario.rossi@example.com about invoice FATT-12345 from 192.168.1.20.

The LLM receives:

Please contact PERSON_001 at EMAIL_001 about invoice INVOICE_001 from IP_001.

The mapping remains local:

PERSON_001 → Mario Rossi EMAIL_001 → mario.rossi@example.com INVOICE_001 → FATT-12345 IP_001 → 192.168.1.20


PuliAI is an experimental project.

Detection is not guaranteed to identify every sensitive value.

Regex rules and semantic labels depend on their configuration and input data.

PuliAI should therefore be considered a preprocessing/pseudonymization layer, not a complete data-loss-prevention system.

Architecture

input/*.{txt,log,csv}
       │
       ▼
  ┌──────────┐
  │  Regex   │  structured entities (email, URL, IP, IBAN, tax code, VAT number, ZIP code)
  └────┬─────┘
       │
       ▼
   ┌───────────┐
   │  GLiNER2  │  semantic entities (person, company, date, ...)
   └─────┬─────┘
       │
       ▼
output/*_anon.{txt,log,csv}

Components

Module Role
config.py Paths, model, labels
regex_rules.py Regex patterns for structured data
anonymizer.py Replacement with sequential IDs
gliner_utils.py Entity recognition via GLiNER2 (BERT) with chunking (regex fallback if unavailable)
main.py Pipeline orchestrator

Anonymizer

The Anonymizer class handles entity replacement using sequential placeholders:

  • EMAIL_001, EMAIL_002, …
  • PERSON_001, PERSON_002, …
  • COMPANY_001, COMPANY_002, …
  • DATE_001, DATE_002, …
  • URL_001, URL_002, …
  • PHONE_001, PHONE_002, …
  • IP_001, IP_002, …
  • IBAN_001, IBAN_002, …

The same value always receives the same ID (intra-document consistency). The mapping is reset on each run.

To restore the original text from an anonymized file using the saved mapping, use restore_text.

Adding a Regex

Each rule is an entry in the REGEX_RULES dictionary in regex_rules.py.

The key (EMAIL, IP, …) becomes the placeholder prefix (EMAIL_001).

Example — Italian tax code regex:

REGEX_RULES = {
    ...
    "CF": r"\b[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]\b",
}

Pattern breakdown:

Part Meaning
\b Word boundary (prevents false positives inside longer words)
[A-Z]{6} 6 uppercase letters (surname + first name)
\d{2} 2 digits (year of birth)
[A-Z] 1 letter (month)
\d{2} 2 digits (day / gender)
[A-Z] 1 letter (municipality code)
\d{3} 3 digits (sequence number)
[A-Z] 1 letter (check character)
\b Word boundary

Adding a new regex requires only one line:

REGEX_RULES = {
    ...
    "INVOICE_CODE": r"\bFATT-\d{5}\b",
}

The engine automatically finds all matches and replaces them with INVOICE_CODE_001, INVOICE_CODE_002, …

Tests

make test

Tests covering:

File Coverage
test_anonymizer.py create_id (sequential, idempotent, separate counters per type), anonymize_text (replacement, reverse order), file persistence
test_regex_rules.py Every regex pattern (valid matches, false positives, edge cases)
test_gliner_utils.py Regex fallback when GLiNER2 is not installed (email, phone, IP, IBAN, unknown label)
test_main.py find_regex_entities, merge_entities (deduplication, ordering), integrated process_file

Requirements

  • Python 3.10+
  • pipenv
make build

GLiNER2 Model

Download the model (205M, BERT-based) for recognizing people, companies, dates, etc.:

pipenv run python -c "from gliner2 import GLiNER2; GLiNER2.from_pretrained('fastino/gliner2-base-v1')"

Without the model, GLiNER2 falls back to regex detection for structured entities only (email, phone, IP, IBAN).

To use a local model path (e.g. for an offline environment), set the following in config.py:

GLINER_MODEL = "./models/gliner2-base-v1"
HF_OFFLINE = True

Then download and save the model manually:

pipenv run python -c "from gliner2 import GLiNER2; GLiNER2.from_pretrained('fastino/gliner2-base-v1').save_pretrained('./models/gliner2-base-v1')"

Usage

mkdir -p input output
echo "Mario Rossi — mario@example.com" > input/test.txt
pipenv run python main.py input/test.txt
cat output/test_anon.txt

Or using Make:

make INPUT=input/test.txt app

Restore

To restore an anonymized file using the generated mapping:

pipenv run python main.py --restore output/test_anon.txt
cat output/test_anon_restored.txt

Or using Make:

make restore INPUT=output/test_anon.txt

About

A local privacy layer that detects and pseudonymizes sensitive data before sending prompts to LLMs.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages