From d263530fb25abb7a28d19c493b9214531aa66891 Mon Sep 17 00:00:00 2001 From: NotYuSheng Date: Tue, 23 Jun 2026 22:49:21 +0800 Subject: [PATCH 01/15] feat: sensitive-data redaction, smarter grouping, OpenAI-compatible LLM, config hygiene Adds a privacy/sanitization layer and improves conversation quality before training. Sensitive-data redaction (new): - ingest/redaction/: locale-keyed regex detector registry (universal + a Singapore pack with NRIC checksum, local phone, postal), mirroring the adapter registry so new countries are a single drop-in module. - ingest/redactor.py: non-destructive scan -> data/redaction_report.json (masked previews), opt-in --redact replace|drop, plus optional LLM verbatim-span detection. - CLI: --redact, --redact-locales, --skip-redact-scan, --llm-redact (with a local-first cloud-consent guard), and --no-audit / --skip-validation off-switches. Conversation grouping: - NormalizedMessage gains message_id/reply_to_id; Telegram adapter populates them. - core: reply-threading stitches gap-split conversations back together; --multi-speaker preserves and labels group-chat senders (the owner's turns are never labelled). - validator: adds a pairing axis and keep/split/drop repair of over-merged samples. LLM client: - ingest/llm.py: shared OpenAI-compatible client (OpenAI or local Ollama/vLLM/LM Studio), replacing the Anthropic SDK. Degrades gracefully if the endpoint is down. - Env vars renamed off the old DialogSmith name: LLM_VALIDATE / LLM_MODEL / LLM_API_KEY / LLM_API_BASE_URL. Config & docs: - train_lora.yaml: explicit train_on_prompt: false to document loss masking (makes --multi-speaker labels safe). - *.local.yaml override pattern (gitignored) keeps personal model/hardware tweaks out of git; .env reconciled to current vars; .env.example renamed to example.env. - README restyled to the project house style; prominent caution + intended/responsible use sections. Tests: adds tests/test_redaction.py and new grouping/validator cases (41 total, green). Co-Authored-By: Claude Opus 4.8 --- .env.example | 12 -- .gitignore | 4 + README.md | 314 +++++++++++++++++++++++----------- configs/train_lora.yaml | 4 + example.env | 31 ++++ ingest/adapters/telegram.py | 4 + ingest/cli.py | 118 ++++++++++++- ingest/core.py | 114 ++++++++++-- ingest/llm.py | 97 +++++++++++ ingest/message.py | 9 + ingest/redaction/__init__.py | 170 ++++++++++++++++++ ingest/redaction/sg.py | 92 ++++++++++ ingest/redaction/universal.py | 76 ++++++++ ingest/redactor.py | 256 +++++++++++++++++++++++++++ ingest/validator.py | 183 +++++++++++--------- requirements.txt | 7 +- setup.bat | 4 +- setup.sh | 4 +- tests/test_ingest.py | 76 +++++++- tests/test_redaction.py | 167 ++++++++++++++++++ 20 files changed, 1527 insertions(+), 215 deletions(-) delete mode 100644 .env.example create mode 100644 example.env create mode 100644 ingest/llm.py create mode 100644 ingest/redaction/__init__.py create mode 100644 ingest/redaction/sg.py create mode 100644 ingest/redaction/universal.py create mode 100644 ingest/redactor.py create mode 100644 tests/test_redaction.py diff --git a/.env.example b/.env.example deleted file mode 100644 index 1179511..0000000 --- a/.env.example +++ /dev/null @@ -1,12 +0,0 @@ -# ── LLM Validation ──────────────────────────────────────────────────────────── -# Validates extracted conversation samples for coherence and quality before -# writing the dataset. Enabled by default when ANTHROPIC_API_KEY is set. -# Set to false to skip validation entirely (faster, no API calls). -DIALOGSMITH_LLM_VALIDATE=true - -# Model used for validation scoring (defaults to claude-haiku-4-5-20251001). -# A fast, cheap model is recommended here — the validator runs once per sample. -DIALOGSMITH_LLM_MODEL=claude-haiku-4-5-20251001 - -# Your Anthropic API key. Required when DIALOGSMITH_LLM_VALIDATE=true. -ANTHROPIC_API_KEY=your_api_key_here diff --git a/.gitignore b/.gitignore index 542add2..2e45380 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ ChatExport*/ # Tracked project config (re-include despite the broad *.json rule above) !configs/dataset_info.json +# Personal training/export overrides — copy a tracked config to *.local.yaml and +# edit that for your own model/hardware; it stays out of git. +configs/*.local.yaml + # Python cache / bytecode __pycache__/ *.py[cod] diff --git a/README.md b/README.md index 292694b..8cf44a7 100644 --- a/README.md +++ b/README.md @@ -1,135 +1,221 @@ -# Doppelganger – Fine-Tune Models on Your Chat History +

Doppelganger

-**Doppelganger** lets you fine-tune large language models (LLMs) like Qwen on your own chat -conversations. Built on top of [LLaMA-Factory](https://github.com/hiyouga/LLaMA-Factory), it -formats your data into the ShareGPT format for supervised fine-tuning (SFT). +

+ Fine-tune an LLM on your own chat history to mimic how you write +

-Ingestion is **source-agnostic**: a small adapter parses each platform's export into a normalized -message stream, and the rest of the pipeline (sessionizing, turn-merging, optional quality -validation, ShareGPT formatting) is shared. **Telegram** is supported today; other sources -(WhatsApp, etc.) are planned and slot in as drop-in adapters — see [issue #9](https://github.com/NotYuSheng/Doppelganger/issues/9). +

+ Features • + Quick Start • + Usage • + Fine-Tuning • + Privacy +

-## Purpose +

+ Python + PyTorch + LLaMA-Factory + OpenAI-compatible + License: MIT +

-Fine-tuning on chat data can capture aspects of your text style, including: +--- -* Writing tone, vocabulary, and phrasing -* Typical response lengths and structure -* Repeated expressions or idioms -* Conversational flow and habits +Doppelganger fine-tunes large language models (like Qwen) on your own chat conversations, capturing how *you* write. Built on top of [LLaMA-Factory](https://github.com/hiyouga/LLaMA-Factory), it turns a raw chat export into a [ShareGPT](https://github.com/hiyouga/LLaMA-Factory/blob/main/data/README.md)-formatted dataset for supervised fine-tuning (SFT), then trains a LoRA adapter on it. -However, this method **won’t replicate your deeper beliefs, private memories, or behavior outside the chat**. It reflects how you write — not necessarily how you think. +Ingestion is **source-agnostic**: a small adapter parses each platform's export into a normalized message stream, and the rest of the pipeline (sessionizing, turn-merging, sensitive-data scanning, optional quality auditing, ShareGPT formatting) is shared. **Telegram** is supported today, with **WhatsApp**, **Discord**, and other platforms planned — each slots in as a drop-in adapter. -For stronger emulation, consider incorporating: +> [!CAUTION] +> **Your chat history is sensitive data, and you are responsible for it.** A model fine-tuned on it can memorize and later reproduce personal identifiers, private conversations, credentials, and things said by other people who never consented. The built-in [sensitive-data scanning](#privacy--sensitive-data) is a **safety net, not a guarantee** — both regex and LLM detection miss real cases and raise false positives. Before training, sharing, or deploying anything, **review the dataset yourself**, obtain any consent you need, and ensure you comply with applicable privacy laws. Treat trained adapters and merged checkpoints as sensitive too — they can leak the data they were trained on. -* Additional sources like emails or forum posts -* Clear prompt instructions during inference -* Domain-specific datasets (e.g., technical messages, inside jokes) +> [!IMPORTANT] +> **This is a for-fun, experimental project — not a production tool.** A model that imitates a real person can be misused for impersonation, deception, or social engineering, and it will happily generate convincing messages that person never actually wrote. Don't present its output as genuinely from anyone, don't train on someone else's chats without their knowledge, and don't rely on it for anything that matters. Enjoy it responsibly. -## Warning: Risk of Sensitive Data Exposure +Fine-tuning on your chats can capture your: -Fine-tuning on real chat history may unintentionally encode: +- **Writing tone, vocabulary, and phrasing** +- **Typical response lengths and structure** +- **Repeated expressions and idioms** +- **Conversational flow and habits** -* Personal identifiers (names, locations, contact info) -* Confidential conversations -* Sensitive or offensive content +> **Note**: This reflects *how you write*, not how you think — it **won't** replicate your deeper beliefs, private memories, or behaviour outside the chat. For stronger emulation, add other sources (emails, forum posts), clear prompt instructions at inference, and domain-specific data (technical messages, inside jokes). -> **Always review and sanitize your exported dataset (`result.json`) before training.** -> You are responsible for ensuring compliance with privacy laws and personal data protection. +## Features -### Keeping your data out of git +| Feature | Description | +|---------|-------------| +| **Source-agnostic ingestion** | One adapter per platform parses an export into a normalized message stream; the rest of the pipeline is shared. Telegram today; others drop in without touching the core. | +| **Conversation reconstruction** | Sessionizes messages by silence gaps **and** reply links, merges consecutive turns, and (optionally) preserves per-speaker labels in group chats. | +| **Sensitive-data scan** | Non-destructive regex scan over the built conversations — email, payment cards (checksum-validated), IP/MAC, API keys, plus pluggable country ID packs. Writes an audit report; you decide what to remove. | +| **LLM redaction** *(optional)* | An OpenAI-compatible model flags context-dependent PII (names, secrets) regex misses, into the same report and apply step. Local-first by design. | +| **LLM quality auditor** *(optional)* | Scores each conversation for coherence, quality, and pairing; drops weak samples and splits over-merged ones. | +| **ShareGPT output** | Emits exactly the format LLaMA-Factory consumes for SFT, with loss masked to your own turns. | +| **LoRA fine-tuning** | Ready-made train / export / chat configs; swap the base model in one place. | -Your chat export and any generated datasets are ignored by `.gitignore` -(`result.json`, `*.json`, `*.jsonl`, `DataExport*/`, `*.session`, `.env`, plus Telegram -media/contacts such as `*.vcard`, `*.tgs`, `*.webp`, `*.ogg`/`*.oga`). Generic -media (`.jpg`, `.mp4`, …) lives inside `DataExport*/`, which is ignored -wholesale. As an extra safeguard, a pre-commit hook refuses to commit these -files even if they are force-added. Enable it once per clone: +## Quick Start -```bash -git config core.hooksPath hooks -``` +### Prerequisites -To deliberately commit a blocked file, bypass the hook with `git commit --no-verify`. +| Software | Version | Purpose | +|----------|---------|---------| +| Python | 3.11–3.13 | Required by LLaMA-Factory 0.9.4 | +| PyTorch | CUDA build for your GPU | Training (see the [install matrix](https://pytorch.org/get-started/locally/)) | +| git | Latest | Clone + the dataset-hygiene pre-commit hook | +| LLM server | Any OpenAI-compatible API | **Optional** — quality auditing & LLM redaction (Ollama, vLLM, LM Studio, OpenAI) | -## Requirements +A CUDA-capable GPU is needed for training. Ingestion (parsing → dataset) runs fine on CPU. -* **Python 3.11–3.13** (required by LLaMA-Factory 0.9.4) -* A CUDA-capable GPU for training, with a matching [PyTorch build](https://pytorch.org/get-started/locally/) -* `git` +### Installation -## Export Your Telegram Chat +**1. Export your Telegram chat** -1. Open **Telegram Desktop**. -2. Go to: `Settings > Advanced > Export Telegram Data`. -3. Select your personal chat or group to export. -4. Ensure **JSON** format is selected (not HTML). -5. Place the exported `result.json` file into: +In **Telegram Desktop**: `Settings > Advanced > Export Telegram Data`. Select your chat(s), choose **JSON** format (not HTML), and place the result here: ``` Doppelganger/ -├── data/ -│ └── result.json ← Place here +└── data/ + └── result.json ← place your export here ``` -## Setup - -The setup scripts create a virtual environment, install pinned dependencies -(LLaMA-Factory **0.9.4**), and process your export into `data/chat_sharegpt.json`. +**2. Clone and run setup** -**Linux / macOS:** +The setup scripts create a virtual environment, install pinned dependencies (LLaMA-Factory **0.9.4**), create your `.env`, and process the export into `data/chat_sharegpt.json`. ```bash -./setup.sh -``` - -**Windows** (from **Command Prompt**, not PowerShell): +git clone https://github.com/NotYuSheng/Doppelganger.git +cd Doppelganger -```cmd -setup.bat +./setup.sh # Linux / macOS +setup.bat # Windows (from Command Prompt, not PowerShell) ``` -Prefer to do it manually? The scripts are thin wrappers around: +
+Prefer to run it manually? ```bash python -m venv venv -# activate: source venv/bin/activate (Windows: venv\Scripts\activate) +source venv/bin/activate # Windows: venv\Scripts\activate pip install -r requirements.txt python -m ingest --source telegram ``` +
-### Ingestion options +**3. (Optional) Configure LLM features** -`python -m ingest` turns a raw export into a dataset. Useful flags: +Copy `example.env` to `.env` (the setup scripts do this for you) and fill it in to enable the quality auditor and LLM redaction. Local endpoints keep your chat data on your machine: -| Flag | Default | Description | -| --------------------- | ------------------------- | ------------------------------------------------------ | -| `--source` | `telegram` | Chat source to parse (more planned) | -| `--input` | `./data/result.json` | Path to the raw export | -| `--format` | `sharegpt` | `sharegpt` (for training) or `jsonl` (intermediate) | -| `--self-name` | auto-detected | Override which sender is "you" | -| `--conversation-gap` | `3600` | Seconds of silence that start a new conversation | -| `--message-chain` | `30` | Max seconds between same-sender messages to merge | +```dotenv +LLM_VALIDATE=true +LLM_MODEL=gpt-4o-mini +LLM_API_KEY=your_api_key_here +# For a local model instead (key can be any value): +# LLM_API_BASE_URL=http://localhost:11434/v1 +# LLM_MODEL=qwen2.5 +``` + +**4. Fine-tune** + +```bash +source venv/bin/activate +llamafactory-cli train configs/train_lora.yaml +``` -### Optional: LLM quality validation +## Usage -Each extracted conversation can be scored for coherence and quality, dropping weak samples before -training. It is enabled automatically when `ANTHROPIC_API_KEY` is set. Copy `.env.example` to `.env` -and fill it in (the setup scripts do this for you): +`python -m ingest` turns a raw export into a training-ready dataset. Useful flags: -```dotenv -DIALOGSMITH_LLM_VALIDATE=true -DIALOGSMITH_LLM_MODEL=claude-haiku-4-5-20251001 -ANTHROPIC_API_KEY=your_api_key_here +| Flag | Default | Description | +|------|---------|-------------| +| `--source` | `telegram` | Chat source to parse (more planned) | +| `--input` | `./data/result.json` | Path to the raw export | +| `--format` | `sharegpt` | `sharegpt` (for training) or `jsonl` (intermediate) | +| `--self-name` | auto-detected | Override which sender is "you" | +| `--conversation-gap` | `3600` | Seconds of silence that start a new conversation | +| `--message-chain` | `30` | Max seconds between same-sender messages to merge into one turn | +| `--multi-speaker` | off | In group chats, keep and label each sender on the human side (your turns are never labelled) | +| `--no-audit` | off | Master off-switch: skip **all** auditing (regex scan + LLM validation) and just build the dataset | +| `--skip-redact-scan` | off | Skip only the regex sensitive-data scan | +| `--skip-validation` | off | Skip only the LLM quality validation | + +### Optional: LLM quality auditing + +Each extracted conversation can be scored for **coherence, quality, and pairing**, dropping or splitting weak samples before training. It uses the OpenAI-compatible API, so it works with OpenAI **or any local server** (Ollama, vLLM, LM Studio). It's enabled automatically when `LLM_API_KEY` or `LLM_API_BASE_URL` is set (configure it in `.env`, step 3 above). + +To turn it off, set `LLM_VALIDATE=false` in `.env` (persistent) or pass `--skip-validation` for a single run. To disable **all** auditing at once — both this and the regex scan — use `--no-audit`. + +## Privacy & Sensitive Data + +Fine-tuning on real chat history may unintentionally encode personal identifiers, confidential conversations, or sensitive content. + +> **Always review and sanitize your dataset before training.** You are responsible for compliance with privacy laws and personal data protection. + +### Automated sensitive-data scan + +To make that review practical, ingestion runs a **regex-based scan** over the built conversations by default. It is **non-destructive** — it only flags and warns, writing `data/redaction_report.json` (with masked previews) and printing a summary so you can decide what to do: + +``` +[redactor] WARNING: 3 potential sensitive item(s) detected across 2 conversations: + EMAIL 2 hit(s) in 2 conversation(s) [medium] + CARD_NUMBER 1 hit(s) in 1 conversation(s) [high] + API_KEY 1 hit(s) in 1 conversation(s) [high] +``` + +Detection works everywhere out of the box. **Universal detectors** — email, payment cards (checksum-validated), IP/MAC addresses, API keys and private keys — aren't tied to any country and always run. On top of those, optional **locale packs** add country-specific identifiers (national IDs, local phone/postal formats). + +Once you've reviewed the report, act on it: + +```bash +python -m ingest --source telegram --redact replace # swap spans for [CATEGORY] +python -m ingest --source telegram --redact drop # drop flagged conversations +python -m ingest --source telegram --skip-redact-scan # opt out entirely +``` + +### Add coverage for your country + +Locale packs are built to be community-contributed: each is a single drop-in module under [`ingest/redaction/`](ingest/redaction/), needing no changes to the scanner or pipeline. Adding one is three steps: + +1. Copy an existing pack to `ingest/redaction/.py` (your ISO country code). +2. Register detectors with `make(...)` and `locale=""`. Back each pattern with a checksum/validator where the identifier has one — that precision is what keeps the report trustworthy instead of noisy. +3. Import your module in `ingest/redaction/__init__.py`. + +Singapore ships as the worked reference ([`sg.py`](ingest/redaction/sg.py): national ID with checksum, local phone, postal code) — but the recipe is the same for any country, and **PRs for new locales are welcome**. Choose which packs run with `--redact-locales` (universal detectors always run regardless). + +### LLM-assisted redaction + +Regex can't catch everything (names, context-dependent secrets). With `--llm-redact`, an LLM additionally flags such spans into the **same report and the same `--redact` step** — it points at verbatim spans, never rewriting your text. To protect your data it **prefers a local endpoint**: set `LLM_API_BASE_URL` to a local OpenAI-compatible server; without one it refuses to use a hosted API unless you pass `--allow-cloud-redaction`. + +```bash +LLM_API_BASE_URL=http://localhost:11434/v1 LLM_MODEL=qwen2.5 \ + python -m ingest --source telegram --llm-redact --redact replace ``` -Set `DIALOGSMITH_LLM_VALIDATE=false` to skip validation entirely (no API calls). +### Keeping your data out of git + +Your chat export and any generated datasets are ignored by `.gitignore` (`result.json`, `*.json`, `*.jsonl`, `DataExport*/`, `*.session`, `.env`, plus Telegram media/contacts such as `*.vcard`, `*.tgs`, `*.webp`, `*.ogg`/`*.oga`). Generic media (`.jpg`, `.mp4`, …) lives inside `DataExport*/`, which is ignored wholesale. As an extra safeguard, a pre-commit hook refuses to commit these files even if they are force-added. Enable it once per clone: + +```bash +git config core.hooksPath hooks +``` + +To deliberately commit a blocked file, bypass the hook with `git commit --no-verify`. + +## Intended Use & Responsible Use + +Doppelganger is a **personal, educational project** — built for individuals to experiment with fine-tuning on **their own** chat history, for fun and learning. It is not a product, and it is **not** intended for profiling or surveilling other people, or for any commercial or deceptive use. + +If you use it, please: + +- **Use your own data.** Train on chats you're a participant in. Group chats include other people's messages — be considerate, and don't publish models trained on them. +- **Keep it local.** Don't publish the dataset, the trained adapter, or merged checkpoints — they can leak the conversations they were trained on. +- **Don't impersonate or deceive.** Never present generated text as something a real person actually said or wrote. +- **Respect the law.** You are responsible for complying with the privacy and data-protection laws in your jurisdiction. + +In short: it's a toy for exploring how *you* write — please keep it that way. ## Fine-Tune Your Model (LoRA) -Training is configured by [`configs/train_lora.yaml`](configs/train_lora.yaml), which defaults to -**Qwen1.5-1.8B-Chat** and the `chat_sharegpt` dataset registered in -[`configs/dataset_info.json`](configs/dataset_info.json). Activate your venv, then run: +Training is configured by [`configs/train_lora.yaml`](configs/train_lora.yaml), which defaults to **Qwen1.5-1.8B-Chat** and the `chat_sharegpt` dataset registered in [`configs/dataset_info.json`](configs/dataset_info.json). Activate your venv, then run: ```bash llamafactory-cli train configs/train_lora.yaml @@ -139,16 +225,27 @@ llamafactory-cli train configs/train_lora.yaml Edit `configs/train_lora.yaml`: -| Field | Description | -| ---------------------- | -------------------------------------------------------- | -| `model_name_or_path` | Hugging Face model ID or local model path | -| `template` | Prompt template type (e.g., `qwen`, `chatml`, `default`) | -| `lora_target` | LoRA target modules (`all` works across architectures) | -| `output_dir` | Destination to save the LoRA checkpoints | +| Field | Description | +|-------|-------------| +| `model_name_or_path` | Hugging Face model ID or local model path | +| `template` | Prompt template type (e.g. `qwen`, `chatml`, `default`) | +| `lora_target` | LoRA target modules (`all` works across architectures) | +| `output_dir` | Destination to save the LoRA checkpoints | + +For example, to use `mistralai/Mistral-7B-Instruct-v0.2`, set `model_name_or_path` accordingly and `template: chatml`. Refer to the [LLaMA-Factory model table](https://github.com/hiyouga/LLaMA-Factory#supported-models) for recommended values. + +> **Note**: Training masks the loss to your own (assistant) turns — `train_on_prompt: false`. That's why `--multi-speaker` labels on the human side are safe: the model reads them as context but never learns to produce them. + +#### Keep personal tweaks out of git -For example, to use `mistralai/Mistral-7B-Instruct-v0.2`, set `model_name_or_path` accordingly and -`template: chatml`. Refer to the -[LLaMA-Factory model table](https://github.com/hiyouga/LLaMA-Factory#supported-models) for recommended values. +The configs above are committed defaults — editing them in place shows up in `git status` and risks committing your machine-specific model/hyperparameters. To customize **without touching tracked files**, copy a config to a `*.local.yaml` name and edit that instead. Any `configs/*.local.yaml` is gitignored: + +```bash +cp configs/train_lora.yaml configs/train_lora.local.yaml # edit model, batch size, etc. +llamafactory-cli train configs/train_lora.local.yaml +``` + +The same works for `export_lora.local.yaml`. Your overrides stay local; the repo's defaults stay clean. ### Resume training @@ -176,7 +273,7 @@ llamafactory-cli chat \ Update `--template` to match the one used during training. -## Activating the environment later +## Activating the Environment Later After running setup once, reactivate the venv in future sessions before running any commands: @@ -185,21 +282,30 @@ source venv/bin/activate # Linux / macOS venv\Scripts\activate # Windows (Command Prompt) ``` -## Running the tests +## Running the Tests -The ingestion pipeline (parsing, sessionizing, turn-merging, ShareGPT formatting) is covered by a -fast unit suite — no GPU, network, or API key required: +The ingestion pipeline (parsing, sessionizing, turn-merging, reply-threading, sensitive-data detection, ShareGPT formatting) is covered by a fast unit suite — no GPU, network, or API key required: ```bash python -m unittest discover -s tests -t . ``` -It runs in well under a second and locks in the conversion behaviour, so you can verify a change -without running the full pipeline. +It runs in well under a second and locks in the conversion behaviour, so you can verify a change without running the full pipeline. + +## Legacy Workflow + +The pre-refactor, Windows-only workflow (which cloned LLaMA-Factory at HEAD) is preserved at the [`v0.1.0`](https://github.com/NotYuSheng/Doppelganger/releases/tag/v0.1.0) tag. The old `scripts/telegram_extract.py` and `scripts/convert_to_sharegpt.py` still work as thin deprecated wrappers around `python -m ingest`, but will be removed in a future release. + +## Star History + + + + + + Star History Chart + + -## Legacy workflow +## License -The pre-refactor, Windows-only workflow (which cloned LLaMA-Factory at HEAD) is preserved at the -[`v0.1.0`](https://github.com/NotYuSheng/Doppelganger/releases/tag/v0.1.0) tag. The old -`scripts/telegram_extract.py` and `scripts/convert_to_sharegpt.py` still work as thin deprecated -wrappers around `python -m ingest`, but will be removed in a future release. +This project is licensed under the MIT License. See [LICENSE](LICENSE) for details. diff --git a/configs/train_lora.yaml b/configs/train_lora.yaml index d1499e5..ff4db98 100644 --- a/configs/train_lora.yaml +++ b/configs/train_lora.yaml @@ -33,6 +33,10 @@ plot_loss: true overwrite_output_dir: true ### train +# Loss is computed only on your (assistant/"gpt") turns; human turns are masked. +# This is the SFT default, set explicitly here so --multi-speaker speaker labels +# on the human side are safe — they condition the model but are never generated. +train_on_prompt: false per_device_train_batch_size: 2 gradient_accumulation_steps: 4 learning_rate: 5.0e-5 diff --git a/example.env b/example.env new file mode 100644 index 0000000..22f142a --- /dev/null +++ b/example.env @@ -0,0 +1,31 @@ +# Copy to .env and fill in. .env is gitignored — never commit your keys. +# Every value here is OPTIONAL; with none set, ingestion still runs (the LLM +# features just stay off). + +# ── Optional LLM features ───────────────────────────────────────────────────── +# Used by the conversation quality auditor and the optional LLM redaction pass. +# Both speak the OpenAI-compatible API, so they work with OpenAI or any local +# server (Ollama, vLLM, LM Studio, llama.cpp). Running a LOCAL model keeps your +# chat data on your machine — the recommended setup for private data. + +# Enable/disable the quality auditor. Default: enabled when LLM_API_KEY or +# LLM_API_BASE_URL is set. Set to false to skip it entirely (no API calls). +LLM_VALIDATE=true + +# Model id. For a local server use whatever it serves (e.g. qwen2.5, llama3.1). +LLM_MODEL=gpt-4o-mini + +# API key. Required for hosted APIs; local servers usually accept any value. +LLM_API_KEY=your_api_key_here + +# OpenAI-compatible endpoint. Set this to use a local model, e.g. +# http://localhost:11434/v1 (Ollama) +# http://localhost:8000/v1 (vLLM) +# Leave unset to use OpenAI's hosted API. +# LLM_API_BASE_URL=http://localhost:11434/v1 + +# ── Optional: Hugging Face ──────────────────────────────────────────────────── +# Only needed to download GATED models during training (e.g. Gemma). The default +# Qwen model in configs/train_lora.yaml is open and needs no token. Read by the +# training stack (huggingface_hub), not by this repo's ingestion code. +# HF_TOKEN= diff --git a/ingest/adapters/telegram.py b/ingest/adapters/telegram.py index dabb2b8..7d0bbae 100644 --- a/ingest/adapters/telegram.py +++ b/ingest/adapters/telegram.py @@ -65,6 +65,8 @@ def parse( if not _is_valid(msg): continue sender = msg.get("from") + reply_to = msg.get("reply_to_message_id") + msg_id = msg.get("id") messages.append( NormalizedMessage( chat_id=chat_id, @@ -72,6 +74,8 @@ def parse( sender_id=sender, sender_is_self=(sender == self_name), text=_get_text(msg), + message_id=str(msg_id) if msg_id is not None else None, + reply_to_id=str(reply_to) if reply_to is not None else None, ) ) return messages diff --git a/ingest/cli.py b/ingest/cli.py index b666f5c..2cf2a5f 100644 --- a/ingest/cli.py +++ b/ingest/cli.py @@ -10,7 +10,9 @@ import os import sys -from ingest import core, sharegpt +import os.path + +from ingest import core, redactor, sharegpt from ingest.adapters import available_sources, get_adapter from ingest.validator import validate_samples @@ -38,6 +40,36 @@ def _load_dotenv(path: str = ".env") -> None: os.environ[key] = value +def _run_llm_redaction(samples, allow_cloud: bool): + """Run the optional LLM redaction pass, guarding against accidental cloud use. + + Returns a (possibly empty) list of LLM findings. Prefers a local endpoint; + if none is configured and cloud use wasn't explicitly allowed, it warns and + skips rather than silently shipping chat data to a third party. + """ + from ingest import llm + + if not llm.is_local() and not allow_cloud: + print( + "[redactor] --llm-redact set but no local endpoint configured. " + "Refusing to send chat data to a hosted API by default. Set " + f"{llm.BASE_URL_ENV} to a local OpenAI-compatible server (Ollama, " + "vLLM, LM Studio, ...), or pass --allow-cloud-redaction to override. " + "Skipping LLM pass." + ) + return [] + + try: + client = llm.get_client() + except (ImportError, EnvironmentError) as e: + print(f"[redactor] LLM redaction unavailable: {e}. Skipping LLM pass.") + return [] + + model = llm.model() + print(f"[redactor] LLM redaction scan via {model} ({llm.endpoint_label()})...") + return redactor.llm_scan_samples(samples, client, model) + + def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="python -m ingest", @@ -85,6 +117,58 @@ def build_parser() -> argparse.ArgumentParser: help=f"Max seconds between same-sender messages to merge into one turn " f"(default: {core.DEFAULT_MESSAGE_CHAIN}).", ) + parser.add_argument( + "--multi-speaker", + action="store_true", + help="In group chats, keep individual senders and label each user turn " + "with their name (e.g. 'Bob: ...'). Your own turns are never labelled. " + "Default collapses the other side into one speaker.", + ) + parser.add_argument( + "--redact", + choices=["off", "replace", "drop"], + default="off", + help="What to do with detected sensitive data. 'off' (default) only " + "scans and writes a report. 'replace' swaps spans for [CATEGORY] " + "placeholders; 'drop' removes conversations containing detections.", + ) + parser.add_argument( + "--redact-locales", + default="SG", + help="Comma-separated locales for sensitive-data detection (universal " + "patterns always run). Default: SG.", + ) + parser.add_argument( + "--skip-redact-scan", + action="store_true", + help="Skip the sensitive-data scan/report entirely.", + ) + parser.add_argument( + "--llm-redact", + action="store_true", + help="Additionally use an LLM to flag context-dependent sensitive data " + "(names, secrets regex misses). Prefers a local endpoint: set " + "LLM_API_BASE_URL, or pass --allow-cloud-redaction to use a hosted API " + "(which sends chat text to a third party).", + ) + parser.add_argument( + "--allow-cloud-redaction", + action="store_true", + help="Permit LLM redaction against a hosted API when no local " + "LLM_API_BASE_URL is configured.", + ) + parser.add_argument( + "--no-audit", + action="store_true", + help="Master off-switch: skip ALL auditing — the regex sensitive-data " + "scan and the LLM quality validation. Just build the dataset.", + ) + parser.add_argument( + "--skip-validation", + action="store_true", + help="Skip only the LLM quality validation (the regex scan still runs " + "unless --skip-redact-scan / --no-audit is also given).", + ) return parser @@ -110,10 +194,40 @@ def main(argv=None) -> int: messages, conversation_gap=args.conversation_gap, message_chain=args.message_chain, + multi_speaker=args.multi_speaker, ) print(f"Extracted {len(samples)} conversation samples.") - samples = validate_samples(samples) + # --no-audit is the master off-switch; the granular flags disable one half. + skip_scan = args.no_audit or args.skip_redact_scan + skip_validation = args.no_audit or args.skip_validation + if args.no_audit: + print("[audit] All auditing disabled (--no-audit) — building dataset as-is.") + + if not skip_scan: + locales = [s.strip() for s in args.redact_locales.split(",") if s.strip()] + report = redactor.scan_samples(samples, locales=locales) + + llm_findings = [] + if args.llm_redact: + llm_findings = _run_llm_redaction(samples, args.allow_cloud_redaction) + redactor.merge_llm_findings(report, llm_findings) + + report_path = os.path.join(os.path.dirname(output) or ".", "redaction_report.json") + redactor.write_report(report, report_path) + redactor.print_summary(report, report_path, mode=args.redact) + if args.redact != "off": + before = len(samples) + samples = redactor.apply( + samples, args.redact, locales=locales, llm_findings=llm_findings + ) + print( + f"[redactor] Applied --redact {args.redact}: " + f"{before} -> {len(samples)} samples." + ) + + if not skip_validation: + samples = validate_samples(samples) if args.format == "sharegpt": written = sharegpt.write_sharegpt(samples, output) diff --git a/ingest/core.py b/ingest/core.py index da50061..c27b6fe 100644 --- a/ingest/core.py +++ b/ingest/core.py @@ -56,6 +56,61 @@ def _split_into_conversations( return conversations +def _merge_by_reply( + conversations: List[List[NormalizedMessage]], +) -> List[List[NormalizedMessage]]: + """Stitch back conversations that a silence gap split but a reply connects. + + A time gap is a guess at where one conversation ends. An explicit reply link + is ground truth: if a message replies to one in an earlier (same-chat) + conversation, they belong together. We union such conversations and re-sort + each merged group chronologically. + + When no message carries reply metadata (``message_id``/``reply_to_id`` all + ``None``), there is nothing to union and the input is returned unchanged — so + sources without reply data keep the pure time-based behaviour. + """ + n = len(conversations) + if n <= 1: + return conversations + + id_to_conv = { + m.message_id: ci + for ci, conv in enumerate(conversations) + for m in conv + if m.message_id is not None + } + if not id_to_conv: + return conversations + + parent = list(range(n)) + + def find(x: int) -> int: + while parent[x] != x: + parent[x] = parent[parent[x]] + x = parent[x] + return x + + def union(a: int, b: int) -> None: + ra, rb = find(a), find(b) + if ra != rb: + parent[max(ra, rb)] = min(ra, rb) + + for ci, conv in enumerate(conversations): + for m in conv: + target = id_to_conv.get(m.reply_to_id) if m.reply_to_id else None + if target is not None and target != ci: + union(ci, target) + + groups: "Dict[int, List[NormalizedMessage]]" = {} + for ci in range(n): + groups.setdefault(find(ci), []).extend(conversations[ci]) + + # Order merged groups by their earliest message so output stays chronological. + ordered_roots = sorted(groups, key=lambda r: min(m.timestamp for m in groups[r])) + return [sorted(groups[r], key=lambda m: m.timestamp) for r in ordered_roots] + + def _collect_turn( conversation: List[NormalizedMessage], start_idx: int, chain_threshold: int ): @@ -82,35 +137,70 @@ def _collect_turn( return texts, j +def _assemble_turns(raw_turns, multi_speaker: bool) -> Sample: + """Turn ``(sender_id, is_self, text)`` runs into role/text turns. + + Roles: the dataset owner is ``assistant`` (this is what the doppelganger + learns to produce, so it is *never* labelled), everyone else is ``user``. + + Default mode merges adjacent same-role runs, so in a group chat several + people on the "other side" collapse into one ``user`` turn. ``multi_speaker`` + instead keeps each speaker distinct and prefixes ``user`` turns with the + sender (``"Bob: ..."``), only merging consecutive runs from the *same* + sender — preserving who-said-what as conditioning context. + """ + turns: Sample = [] + last_sender = None + + for sender_id, is_self, text in raw_turns: + role = "assistant" if is_self else "user" + value = f"{sender_id}: {text}" if (multi_speaker and role == "user") else text + + same_role = bool(turns) and turns[-1]["role"] == role + # In multi-speaker mode a user turn only merges with the previous turn + # when it is the same speaker; otherwise distinct speakers stay distinct. + mergeable = same_role and not ( + multi_speaker and role == "user" and last_sender != sender_id + ) + if mergeable: + turns[-1]["text"] += "\n" + value + else: + turns.append({"role": role, "text": value}) + last_sender = sender_id + + return turns + + def build_samples( messages: Iterable[NormalizedMessage], conversation_gap: int = DEFAULT_CONVERSATION_GAP, message_chain: int = DEFAULT_MESSAGE_CHAIN, + multi_speaker: bool = False, ) -> List[Sample]: """Turn normalized messages into multi-turn conversation samples. - Splits each chat into conversations, merges consecutive same-sender messages - into turns, and keeps only conversations containing at least one user turn - and one assistant turn. + Splits each chat into conversations (stitching reply-linked ones back + together), merges consecutive same-sender messages into turns, and keeps + only conversations containing at least one user turn and one assistant turn. + + ``multi_speaker`` preserves and labels individual senders in group chats + (see :func:`_assemble_turns`); the default collapses the other side. """ samples: List[Sample] = [] for chat_messages in _group_by_chat(messages): - for conversation in _split_into_conversations(chat_messages, conversation_gap): - turns: Sample = [] + time_convs = _split_into_conversations(chat_messages, conversation_gap) + for conversation in _merge_by_reply(time_convs): + raw_turns = [] i = 0 while i < len(conversation): texts, next_i = _collect_turn(conversation, i, message_chain) if texts: - role = "assistant" if conversation[i].sender_is_self else "user" - turn_text = "\n".join(texts) - # Merge with previous turn if same role (e.g. gap split a block). - if turns and turns[-1]["role"] == role: - turns[-1]["text"] += "\n" + turn_text - else: - turns.append({"role": role, "text": turn_text}) + m = conversation[i] + raw_turns.append((m.sender_id, m.sender_is_self, "\n".join(texts))) i = next_i + turns = _assemble_turns(raw_turns, multi_speaker) roles = {t["role"] for t in turns} if "user" in roles and "assistant" in roles: samples.append(turns) diff --git a/ingest/llm.py b/ingest/llm.py new file mode 100644 index 0000000..b862acf --- /dev/null +++ b/ingest/llm.py @@ -0,0 +1,97 @@ +"""Shared OpenAI-compatible LLM client. + +One client for every optional LLM feature (quality validation, LLM redaction). +It speaks the OpenAI Chat Completions API, so it works against OpenAI itself +*and* any local/self-hosted server that exposes that API — Ollama, vLLM, LM +Studio, llama.cpp's server, LiteLLM, etc. Running a local endpoint is the +privacy-preserving way to use these features, since your chat text never leaves +your machine. + +Environment variables: + LLM_VALIDATE true/false. Default: enabled when LLM_API_KEY or + LLM_API_BASE_URL is set, disabled otherwise. + LLM_API_BASE_URL OpenAI-compatible base URL. Set this for a local model, e.g. + http://localhost:11434/v1 (Ollama) or http://localhost:8000/v1 + (vLLM). Unset → OpenAI's hosted API. + LLM_MODEL Model id (default: gpt-4o-mini). For a local server use whatever + it serves, e.g. "qwen2.5" or "llama3.1". + LLM_API_KEY API key. Local servers usually accept any value; falls back to + OPENAI_API_KEY if unset. +""" + +import os + +VALIDATE_ENV = "LLM_VALIDATE" +MODEL_ENV = "LLM_MODEL" +BASE_URL_ENV = "LLM_API_BASE_URL" +API_KEY_ENV = "LLM_API_KEY" +DEFAULT_MODEL = "gpt-4o-mini" + + +def base_url() -> str: + return os.environ.get(BASE_URL_ENV, "").strip() + + +def model() -> str: + return os.environ.get(MODEL_ENV, "").strip() or DEFAULT_MODEL + + +def is_local() -> bool: + """True when a custom (presumably local/self-hosted) endpoint is configured.""" + return bool(base_url()) + + +def _api_key() -> str: + return ( + os.environ.get(API_KEY_ENV, "").strip() + or os.environ.get("OPENAI_API_KEY", "").strip() + ) + + +def should_validate() -> bool: + val = os.environ.get(VALIDATE_ENV, "").strip().lower() + if val == "false": + return False + if val == "true": + return True + # Default: enable when there's something to talk to. + return bool(_api_key() or base_url()) + + +def get_client(): + """Build an OpenAI-compatible client. Raises if unusable (caller handles).""" + try: + from openai import OpenAI + except ImportError: + raise ImportError( + "The 'openai' package is required for LLM features. " + "Install it with: pip install openai" + ) + url = base_url() + key = _api_key() + if not key: + if url: + key = "not-needed" # local servers ignore it, but the SDK requires a value + else: + raise EnvironmentError( + f"{API_KEY_ENV} is not set. Set it, point {BASE_URL_ENV} at a " + f"local endpoint, or set {VALIDATE_ENV}=false." + ) + kwargs = {"api_key": key} + if url: + kwargs["base_url"] = url + return OpenAI(**kwargs) + + +def endpoint_label() -> str: + return base_url() or "OpenAI API" + + +def chat(client, model_name: str, prompt: str, max_tokens: int = 256) -> str: + """Single-prompt completion; returns the assistant message text.""" + resp = client.chat.completions.create( + model=model_name, + max_tokens=max_tokens, + messages=[{"role": "user", "content": prompt}], + ) + return (resp.choices[0].message.content or "").strip() diff --git a/ingest/message.py b/ingest/message.py index edde67d..0db1df7 100644 --- a/ingest/message.py +++ b/ingest/message.py @@ -1,6 +1,7 @@ """The normalized, source-agnostic message shape shared by the pipeline.""" from dataclasses import dataclass +from typing import Optional @dataclass @@ -24,6 +25,12 @@ class NormalizedMessage: ("you"). Drives the user/assistant role assignment downstream. text: The plain-text message body (already extracted/cleaned by the adapter). Adapters should only emit messages with non-empty text. + message_id: Source-stable id for this message, used to resolve reply + links. ``None`` if the source has no message ids. + reply_to_id: ``message_id`` of the message this one replies to, or + ``None``. Lets the pipeline thread replies instead of relying on + time order alone. Adapters that lack reply data leave both ``None``, + and grouping falls back to its time-based behaviour. """ chat_id: str @@ -31,3 +38,5 @@ class NormalizedMessage: sender_id: str sender_is_self: bool text: str + message_id: Optional[str] = None + reply_to_id: Optional[str] = None diff --git a/ingest/redaction/__init__.py b/ingest/redaction/__init__.py new file mode 100644 index 0000000..a1f93d7 --- /dev/null +++ b/ingest/redaction/__init__.py @@ -0,0 +1,170 @@ +"""Regex-based sensitive-data detection. + +A *detector* is a named, locale-tagged regex (optionally backed by a checksum +validator) that flags one category of sensitive data — an email, a credit card, +a Singapore NRIC, etc. Detectors register themselves at import time via +:func:`register`, exactly like source adapters do, so adding coverage for a new +country is a single drop-in module under ``ingest/redaction/`` — no changes to +the scanner or the pipeline. + +Detection is **non-destructive**: :func:`scan_text` and :func:`scan_samples` +only *report* matches (as :class:`Finding` objects). Whether to redact is the +user's decision, taken later against the audit report. + +Want to add your country? Copy ``sg.py``, swap in your locale's patterns + +checksum validators, and register them. See ``CONTRIBUTING`` notes in ``sg.py``. +""" + +import re +from dataclasses import dataclass +from typing import Callable, Dict, Iterable, List, Optional, Pattern + +UNIVERSAL = "universal" # locale tag for patterns that are the same worldwide + + +@dataclass(frozen=True) +class Detector: + """One category of sensitive data and how to recognise it. + + Attributes: + name: Unique id, e.g. ``"sg_nric"`` or ``"email"``. + category: Human-facing label shown in reports, e.g. ``"NRIC"``. + locale: ``"universal"`` or an ISO 3166-1 alpha-2 code (``"SG"``). + pattern: Compiled regex. Every full match is a candidate. + severity: ``"low" | "medium" | "high"`` — drives the suggested action. + validator: Optional extra check on the matched string (e.g. Luhn, + NRIC checksum). A candidate is only flagged if it returns True. + This is what turns a noisy regex into a high-precision detector. + """ + + name: str + category: str + locale: str + pattern: Pattern + severity: str = "medium" + validator: Optional[Callable[[str], bool]] = None + + +@dataclass(frozen=True) +class Finding: + """A single detected span of sensitive data within one text.""" + + detector: str + category: str + locale: str + severity: str + start: int + end: int + value: str + preview: str # masked, safe to print/log + + +_REGISTRY: "List[Detector]" = [] + + +def register(detector: Detector) -> Detector: + """Register a detector. Duplicate ``name`` is a programming error.""" + if any(d.name == detector.name for d in _REGISTRY): + raise ValueError(f"Duplicate detector name: {detector.name!r}") + _REGISTRY.append(detector) + return detector + + +def make( + name: str, + category: str, + locale: str, + regex: str, + *, + severity: str = "medium", + flags: int = 0, + validator: Optional[Callable[[str], bool]] = None, +) -> Detector: + """Compile a regex and register it as a detector in one call.""" + return register( + Detector( + name=name, + category=category, + locale=locale, + pattern=re.compile(regex, flags), + severity=severity, + validator=validator, + ) + ) + + +def available_locales() -> "List[str]": + return sorted({d.locale for d in _REGISTRY}) + + +def iter_detectors(locales: Optional[Iterable[str]] = None) -> "List[Detector]": + """Detectors for the given locales. ``None`` means all. + + ``UNIVERSAL`` detectors are always included — email/card/IP look the same + everywhere, so they run regardless of which country was selected. + """ + if locales is None: + return list(_REGISTRY) + wanted = {UNIVERSAL, *locales} + return [d for d in _REGISTRY if d.locale in wanted] + + +def mask(value: str) -> str: + """Mask a value for safe display in a report (keep shape, hide content).""" + if "@" in value: # email: keep first char + domain + local, _, domain = value.partition("@") + head = local[0] if local else "" + return f"{head}***@{domain}" + stripped = value.strip() + if len(stripped) <= 4: + return "*" * len(stripped) + return f"{stripped[:2]}{'*' * (len(stripped) - 3)}{stripped[-1]}" + + +def scan_text(text: str, locales: Optional[Iterable[str]] = None) -> "List[Finding]": + """Return all sensitive-data findings in ``text`` (non-destructive).""" + findings: List[Finding] = [] + for det in iter_detectors(locales): + # A detector may match surrounding context but expose only the sensitive + # span via a named ``id`` group (e.g. require "NRIC" before the number, + # but report just the number). Otherwise the whole match is the value. + report_id = "id" in det.pattern.groupindex + for m in det.pattern.finditer(text): + value = m.group("id") if report_id else m.group() + if det.validator and not det.validator(value): + continue + start, end = m.span("id") if report_id else m.span() + findings.append( + Finding( + detector=det.name, + category=det.category, + locale=det.locale, + severity=det.severity, + start=start, + end=end, + value=value, + preview=mask(value), + ) + ) + return findings + + +def luhn_valid(number: str) -> bool: + """Luhn checksum — filters most non-card digit runs (phone/IDs/etc).""" + digits = [int(c) for c in number if c.isdigit()] + if len(digits) < 13 or len(digits) > 19: + return False + total = 0 + for i, d in enumerate(reversed(digits)): + if i % 2 == 1: + d *= 2 + if d > 9: + d -= 9 + total += d + return total % 10 == 0 + + +# Importing the package registers the bundled detectors. Add a new locale module +# here (and as a file) and its detectors light up everywhere automatically. +from ingest.redaction import universal as _universal # noqa: E402,F401 +from ingest.redaction import sg as _sg # noqa: E402,F401 diff --git a/ingest/redaction/sg.py b/ingest/redaction/sg.py new file mode 100644 index 0000000..7b57cdb --- /dev/null +++ b/ingest/redaction/sg.py @@ -0,0 +1,92 @@ +"""Singapore (SG) sensitive-data detectors. + +This is the reference locale module — copy it to add your own country. + +A good locale detector is *precise*: a bare regex over chat text fires on +everything, so back it with a checksum/validator wherever the identifier has one +(see :func:`nric_valid`). High precision is what keeps the audit report +trustworthy instead of a wall of false positives. + +CONTRIBUTING +------------ +Add a country by creating ``ingest/redaction/.py`` (``cc`` = ISO 3166-1 +alpha-2, lower-case), registering detectors with :func:`ingest.redaction.make` +and ``locale=""``, then importing it in ``ingest/redaction/__init__``. +Open items for SG that make good first contributions: + * NRIC **M-series** (introduced 2022) uses a different checksum table — the + regex below intentionally matches only S/T/F/G so it never flags an + M-series number it can't verify. Add the M table + tests. + * UEN (business registration number) detector. +""" + +import re + +from ingest.redaction import make + +# NRIC/FIN checksum tables, indexed by (weighted_sum + offset) % 11. +_ST_SUFFIX = "JZIHGFEDCBA" # S (citizen) and T (citizen, 2000+) +_FG_SUFFIX = "XWUTRQPNMLK" # F and G (foreigner / long-term pass) +_WEIGHTS = (2, 7, 6, 5, 4, 3, 2) + + +def nric_valid(value: str) -> bool: + """Validate a Singapore NRIC/FIN by its check digit (S/T/F/G series).""" + value = value.strip().upper() + if len(value) != 9: + return False + prefix, digits, suffix = value[0], value[1:8], value[8] + if prefix not in "STFG" or not digits.isdigit(): + return False + total = sum(int(d) * w for d, w in zip(digits, _WEIGHTS)) + if prefix in "TG": # T and G shift the weighted sum by 4 + total += 4 + table = _ST_SUFFIX if prefix in "ST" else _FG_SUFFIX + return table[total % 11] == suffix + + +# Long form: full S/T/F/G + 7 digits + check letter, verified by checksum. +# Case-insensitive so "s1234567a" typed in lower-case is still caught (the +# validator upper-cases before checking). +make( + "sg_nric", + "NRIC/FIN", + "SG", + r"\b[STFG]\d{7}[A-Z]\b", + severity="high", + flags=re.IGNORECASE, + validator=nric_valid, +) + +# Short form: the last 3 digits + check letter (e.g. "123A"), the way people +# quote "the last 4 of my IC". It has no self-contained checksum and "123A" +# alone matches every block/unit number, so precision comes from REQUIRING an +# NRIC/IC/FIN keyword just before it. Only the ID span (named group) is +# reported, not the keyword. +make( + "sg_nric_short", + "NRIC/FIN (partial)", + "SG", + r"(?:nric|fin|\bic\b)\D{0,8}?(?P(?" or "S(code)" context to keep precision up. +make( + "sg_postal", + "POSTAL_CODE", + "SG", + r"(?:[Ss]ingapore\s+|\bS\()\d{6}\)?", + severity="low", +) diff --git a/ingest/redaction/universal.py b/ingest/redaction/universal.py new file mode 100644 index 0000000..7db1444 --- /dev/null +++ b/ingest/redaction/universal.py @@ -0,0 +1,76 @@ +"""Locale-independent detectors: same format the world over. + +Email, payment cards, IP/MAC addresses, and vendor API keys don't change by +country, so they live here and always run. Country-specific catches (national +IDs, local phone formats, postal codes) belong in a per-locale module instead. +""" + +import re + +from ingest.redaction import UNIVERSAL, luhn_valid, make + +# --- Contact / network ------------------------------------------------------- + +make( + "email", + "EMAIL", + UNIVERSAL, + r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b", + severity="medium", +) + +make( + "ipv4", + "IP_ADDRESS", + UNIVERSAL, + r"\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b", + severity="low", +) + +make( + "ipv6", + "IP_ADDRESS", + UNIVERSAL, + r"\b(?:[A-Fa-f0-9]{1,4}:){2,7}[A-Fa-f0-9]{1,4}\b", + severity="low", +) + +make( + "mac", + "MAC_ADDRESS", + UNIVERSAL, + r"\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b", + severity="low", +) + +# --- Financial --------------------------------------------------------------- + +# Broad 13–19 digit run (optionally space/dash grouped); Luhn rejects the noise. +make( + "credit_card", + "CARD_NUMBER", + UNIVERSAL, + r"\b(?:\d[ -]?){13,19}\b", + severity="high", + validator=luhn_valid, +) + +# --- Secrets / credentials --------------------------------------------------- + +make("openai_key", "API_KEY", UNIVERSAL, r"\bsk-[A-Za-z0-9]{20,}\b", severity="high") +make("aws_access_key", "API_KEY", UNIVERSAL, r"\bAKIA[0-9A-Z]{16}\b", severity="high") +make( + "github_token", + "API_KEY", + UNIVERSAL, + r"\bgh[pousr]_[A-Za-z0-9]{36,}\b", + severity="high", +) +make( + "private_key_block", + "PRIVATE_KEY", + UNIVERSAL, + r"-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----", + severity="high", + flags=re.IGNORECASE, +) diff --git a/ingest/redactor.py b/ingest/redactor.py new file mode 100644 index 0000000..39786fa --- /dev/null +++ b/ingest/redactor.py @@ -0,0 +1,256 @@ +"""Non-destructive sensitive-data audit over conversation samples. + +Runs the regex detectors in :mod:`ingest.redaction` across every turn, writes an +audit report, and prints a warning summary. By default **nothing is changed** — +the user reviews the report and decides whether to act. Acting is opt-in via +:func:`apply` (wired to the CLI's ``--redact`` flag): + + - "replace": swap each detected span for a ``[CATEGORY]`` placeholder, keeping + conversational structure intact for training. + - "drop": discard any conversation that contains a detected item. + +Detection is regex-based and locale-aware (Singapore-first); see +``ingest/redaction`` to add coverage for more countries. +""" + +import json +import re +from collections import defaultdict +from typing import Iterable, List, Optional + +from ingest import redaction + +DEFAULT_LOCALES = ["SG"] # universal detectors always run in addition to these + + +def scan_samples(samples, locales: Optional[Iterable[str]] = None) -> dict: + """Scan every turn and return an audit report (no mutation).""" + if locales is None: + locales = DEFAULT_LOCALES + + findings = [] + for ci, turns in enumerate(samples): + for ti, turn in enumerate(turns): + for f in redaction.scan_text(turn.get("text", ""), locales): + findings.append({ + "conversation": ci, + "turn": ti, + "role": turn.get("role"), + "category": f.category, + "detector": f.detector, + "severity": f.severity, + "preview": f.preview, + }) + + summary = {} + convs_per_cat = defaultdict(set) + for f in findings: + s = summary.setdefault( + f["category"], {"hits": 0, "conversations": 0, "severity": f["severity"]} + ) + s["hits"] += 1 + convs_per_cat[f["category"]].add(f["conversation"]) + for cat, s in summary.items(): + s["conversations"] = len(convs_per_cat[cat]) + + return { + "conversations_scanned": len(samples), + "total_findings": len(findings), + "locales": list(locales), + "summary": summary, + "findings": findings, + } + + +def write_report(report: dict, path: str) -> None: + with open(path, "w", encoding="utf-8") as f: + json.dump(report, f, indent=2, ensure_ascii=False) + + +def print_summary(report: dict, report_path: str, mode: str = "off") -> None: + n = report["total_findings"] + if n == 0: + print("[redactor] No sensitive data detected by regex scan.") + return + print( + f"[redactor] WARNING: {n} potential sensitive item(s) detected across " + f"{report['conversations_scanned']} conversations:" + ) + for cat, s in sorted(report["summary"].items(), key=lambda kv: -kv[1]["hits"]): + print( + f" {cat:22s} {s['hits']:4d} hit(s) in {s['conversations']:3d} " + f"conversation(s) [{s['severity']}]" + ) + print(f"[redactor] Full report: {report_path}") + if mode == "off": + print( + "[redactor] Nothing was removed. Review it, then re-run with " + "--redact replace (placeholder) or --redact drop (remove conversations)." + ) + + +def _replace_spans(text: str, spans) -> str: + """Replace ``(start, end, category)`` spans with ``[CATEGORY]`` placeholders. + + Drops overlapping spans (keeping the right-most) and replaces right-to-left + so each replacement leaves earlier offsets valid. + """ + chosen = [] + boundary = len(text) + 1 # left edge of the span accepted to our right + for start, end, cat in sorted(set(spans), key=lambda s: -s[0]): + if end <= boundary: + chosen.append((start, end, cat)) + boundary = start + for start, end, cat in chosen: # already right-to-left + text = text[:start] + f"[{cat}]" + text[end:] + return text + + +def apply(samples, mode: str, locales: Optional[Iterable[str]] = None, + llm_findings: Optional[List[dict]] = None) -> List: + """Return samples with detected data handled per ``mode``. + + ``mode`` is "replace" (swap spans for ``[CATEGORY]``) or "drop" (remove any + conversation containing a detection). Regex spans are re-derived per turn; + optional ``llm_findings`` (which carry their own offsets) are applied too. + """ + if locales is None: + locales = DEFAULT_LOCALES + + llm_by_turn = defaultdict(list) + for f in llm_findings or []: + llm_by_turn[(f["conversation"], f["turn"])].append( + (f["start"], f["end"], f["category"]) + ) + + out = [] + for ci, turns in enumerate(samples): + new_turns = [] + drop = False + for ti, turn in enumerate(turns): + text = turn.get("text", "") + spans = [(f.start, f.end, f.category) for f in redaction.scan_text(text, locales)] + spans += llm_by_turn.get((ci, ti), []) + if not spans: + new_turns.append(turn) + continue + if mode == "drop": + drop = True + break + replaced = dict(turn) + replaced["text"] = _replace_spans(text, spans) + new_turns.append(replaced) + if not drop: + out.append(new_turns) + return out + + +# --- Optional LLM detector (Tier 3) ------------------------------------------ +# +# Regex can't catch names or context-dependent secrets. When enabled, the LLM +# reads each conversation and points at sensitive spans *verbatim* (it never +# rewrites the text — that stays the user's decision). Findings flow into the +# same report and the same apply() step as the regex tier. The client/endpoint +# plumbing (incl. LLM_API_BASE_URL for local servers) is shared with the quality +# validator via ingest.llm. + +_LLM_PROMPT = """You are a privacy auditor. Identify spans of SENSITIVE or +PERSONALLY IDENTIFYING information in the conversation below: real people's +names, contact details, addresses, financial or government IDs, credentials, +or health/legal/financial specifics that could identify someone. + +Each turn is numbered "[i] ROLE: text". Do NOT rewrite anything. For each +finding, copy the offending substring EXACTLY as it appears so it can be located. + +Respond with ONLY this JSON: +{{"findings": [{{"turn": , "text": "", "category": "", "severity": "low|medium|high"}}]}} + +Conversation: +{conversation}""" + + +def _format_conversation(turns) -> str: + return "\n".join( + f"[{i}] {t.get('role', '?').upper()}: {t.get('text', '').strip()}" + for i, t in enumerate(turns) + ) + + +def _llm_audit_conversation(client, model, turns) -> List[dict]: + from ingest import llm + + prompt = _LLM_PROMPT.format(conversation=_format_conversation(turns)) + raw = llm.chat(client, model, prompt, max_tokens=512) + match = re.search(r"\{.*\}", raw, re.DOTALL) + if not match: + raise ValueError(f"No JSON object in LLM response: {raw!r}") + return json.loads(match.group()).get("findings", []) + + +def llm_scan_samples(samples, client, model) -> List[dict]: + """LLM pass returning verbatim-located findings (with offsets, in memory). + + Each finding is verified by locating the model's span in the turn text; a + paraphrased span that can't be found is reported as a soft-miss and skipped + rather than trusting an offset we can't confirm. + """ + findings = [] + for ci, turns in enumerate(samples): + try: + raw = _llm_audit_conversation(client, model, turns) + except Exception as e: + print(f"[redactor] LLM scan failed on conversation {ci}: {e}") + continue + for rf in raw: + try: + ti = int(rf["turn"]) + span = str(rf["text"]) + except (KeyError, ValueError, TypeError): + continue + if not (0 <= ti < len(turns)) or not span: + continue + text = turns[ti].get("text", "") + idx = text.find(span) + if idx < 0: + print(f"[redactor] LLM span not found verbatim (conv {ci}, turn {ti}): {span!r}") + continue + findings.append({ + "conversation": ci, + "turn": ti, + "role": turns[ti].get("role"), + "category": str(rf.get("category", "PII")), + "detector": "llm", + "severity": str(rf.get("severity", "medium")), + "start": idx, + "end": idx + len(span), + "preview": redaction.mask(span), + }) + return findings + + +def merge_llm_findings(report: dict, llm_findings: List[dict]) -> dict: + """Fold LLM findings into a regex report (masked previews only; no raw spans).""" + for f in llm_findings: + report["findings"].append({ + "conversation": f["conversation"], + "turn": f["turn"], + "role": f["role"], + "category": f["category"], + "detector": "llm", + "severity": f["severity"], + "preview": f["preview"], + }) + convs_per_cat = defaultdict(set) + for f in report["findings"]: + convs_per_cat[f["category"]].add(f["conversation"]) + summary = {} + for f in report["findings"]: + s = summary.setdefault( + f["category"], {"hits": 0, "conversations": 0, "severity": f["severity"]} + ) + s["hits"] += 1 + for cat, s in summary.items(): + s["conversations"] = len(convs_per_cat[cat]) + report["summary"] = summary + report["total_findings"] = len(report["findings"]) + return report diff --git a/ingest/validator.py b/ingest/validator.py index b702ddd..7a684c2 100644 --- a/ingest/validator.py +++ b/ingest/validator.py @@ -1,149 +1,178 @@ """ -Optional LLM-based conversation quality validator. +Optional LLM-based conversation auditor. -Controlled via environment variables: - DIALOGSMITH_LLM_VALIDATE=true/false (default: true if ANTHROPIC_API_KEY is set) - DIALOGSMITH_LLM_MODEL=... (default: claude-haiku-4-5-20251001) - ANTHROPIC_API_KEY=... +Uses the shared OpenAI-compatible client (see :mod:`ingest.llm`), so it runs +against OpenAI or any local server. Controlled by the ``LLM_*`` environment +variables documented there (``LLM_VALIDATE``, ``LLM_API_BASE_URL``, ``LLM_MODEL``, +``LLM_API_KEY``). -Each conversation sample is scored on two axes: +Each conversation sample is audited on three axes: - coherence: does this read as a natural, continuous conversation? - quality: is this a meaningful exchange worth training on? + - pairing: does each assistant turn actually respond to what came before? -Samples that fail either check are excluded from the output. -A summary of filtered samples is printed so the user can audit decisions. +Because the heuristic grouper can over-merge, the auditor may also *repair* a +sample by proposing split points rather than only keeping or dropping it: + - action "keep": use as-is + - action "split": cut after the given turn indices into independent samples + - action "drop": discard entirely + +A summary of every decision is printed so the user can audit the auditor. """ import json -import os import re -VALIDATE_ENV = "DIALOGSMITH_LLM_VALIDATE" -MODEL_ENV = "DIALOGSMITH_LLM_MODEL" -DEFAULT_MODEL = "claude-haiku-4-5-20251001" - -COHERENCE_THRESHOLD = 0.5 # 0–1, below this the conversation is considered incoherent -QUALITY_THRESHOLD = 0.5 # 0–1, below this the sample is considered low-quality - - -def _should_validate(): - val = os.environ.get(VALIDATE_ENV, "").strip().lower() - if val == "false": - return False - if val == "true": - return True - # Default: enable if API key is present - return bool(os.environ.get("ANTHROPIC_API_KEY", "").strip()) +from ingest import llm - -def _get_client(): - try: - import anthropic - except ImportError: - raise ImportError( - "The 'anthropic' package is required for LLM validation. " - "Install it with: pip install anthropic" - ) - api_key = os.environ.get("ANTHROPIC_API_KEY", "").strip() - if not api_key: - raise EnvironmentError( - "ANTHROPIC_API_KEY is not set. " - f"Set {VALIDATE_ENV}=false to disable validation." - ) - return anthropic.Anthropic(api_key=api_key) +COHERENCE_THRESHOLD = 0.5 # below this the conversation is considered incoherent +QUALITY_THRESHOLD = 0.5 # below this the sample is considered low-quality +PAIRING_THRESHOLD = 0.5 # below this the turns don't respond to each other def _format_conversation(turns): + """Number every turn so the model can reference split points by index.""" lines = [] - for turn in turns: + for i, turn in enumerate(turns): role = turn.get("role", "unknown").upper() text = turn.get("text", "").strip() - lines.append(f"{role}: {text}") + lines.append(f"[{i}] {role}: {text}") return "\n".join(lines) def _score_sample(client, model, turns): - """ - Ask the LLM to score a conversation sample. - Returns (coherence: float, quality: float, reason: str). + """Ask the LLM to audit a conversation sample. + + Returns a dict: coherence, quality, pairing (floats), action + ("keep"|"split"|"drop"), split_after (list[int]), reason (str). """ conversation_text = _format_conversation(turns) - prompt = f"""You are evaluating a conversation sample for use in fine-tuning a language model. + prompt = f"""You are auditing a conversation sample for fine-tuning a language model +to imitate the ASSISTANT speaker. The conversation was segmented by a heuristic +that can wrongly merge unrelated exchanges, so judge it carefully. -Rate the following conversation on two dimensions, each from 0.0 to 1.0: +Each turn is numbered like "[i] ROLE: text". -1. coherence: Does this read as a natural, continuous conversation where each message follows logically from the previous? (0 = completely disjointed, 1 = perfectly coherent) -2. quality: Is this a meaningful, substantive exchange worth training on? Penalise one-word replies, pure greetings, or exchanges with no informational content. (0 = worthless, 1 = highly valuable) +Rate from 0.0 to 1.0: +1. coherence: does this read as one natural, continuous conversation? +2. quality: is this a meaningful exchange worth training on? Penalise pure + greetings, one-word replies, and content-free chatter. +3. pairing: does each ASSISTANT turn actually respond to the USER turn(s) before + it? (0 = replies are mismatched/non-sequiturs, 1 = every reply clearly fits) -Respond with ONLY a JSON object in this exact format: -{{"coherence": , "quality": , "reason": ""}} +Then choose an action: +- "keep": the sample is good as one conversation. +- "split": it is really two or more separate conversations. Give "split_after" + as the list of turn indices AFTER which to cut (e.g. [3] cuts between turn 3 + and 4). +- "drop": it is not usable. + +Respond with ONLY this JSON: +{{"coherence": , "quality": , "pairing": , + "action": "keep"|"split"|"drop", "split_after": [...], "reason": ""}} Conversation: {conversation_text}""" - response = client.messages.create( - model=model, - max_tokens=128, - messages=[{"role": "user", "content": prompt}], - ) - - raw = response.content[0].text.strip() - # The model may wrap the JSON in markdown fences or prose; extract the object. + raw = llm.chat(client, model, prompt, max_tokens=200) match = re.search(r"\{.*\}", raw, re.DOTALL) if not match: raise ValueError(f"No JSON object found in LLM response: {raw!r}") result = json.loads(match.group()) - return float(result["coherence"]), float(result["quality"]), result.get("reason", "") + return { + "coherence": float(result["coherence"]), + "quality": float(result["quality"]), + "pairing": float(result.get("pairing", 1.0)), + "action": str(result.get("action", "keep")).lower(), + "split_after": [int(i) for i in result.get("split_after", []) or []], + "reason": result.get("reason", ""), + } + + +def _apply_split(turns, split_after): + """Cut ``turns`` after each given index into independent samples.""" + cuts = sorted({i for i in split_after if 0 <= i < len(turns) - 1}) + if not cuts: + return [turns] + pieces, start = [], 0 + for idx in cuts: + pieces.append(turns[start:idx + 1]) + start = idx + 1 + pieces.append(turns[start:]) + return [p for p in pieces if p] + + +def _has_both_roles(turns): + roles = {t["role"] for t in turns} + return "user" in roles and "assistant" in roles def validate_samples(samples): """ - Validate a list of conversation samples. + Audit a list of conversation samples. Each sample is a list of {"role": ..., "text": ...} dicts (as produced by ingest.core.build_samples). - Returns filtered list of samples that pass validation. - If validation is disabled or unavailable, returns all samples unchanged. + Returns the filtered/repaired list of samples. If validation is disabled or + unavailable, returns all samples unchanged. """ - if not _should_validate(): + if not llm.should_validate(): print("[validator] LLM validation disabled — skipping.") return samples try: - client = _get_client() + client = llm.get_client() except (ImportError, EnvironmentError) as e: print(f"[validator] WARNING: {e}") print("[validator] Skipping LLM validation and returning all samples.") return samples - model = os.environ.get(MODEL_ENV, DEFAULT_MODEL).strip() - print(f"[validator] Running LLM validation with model: {model}") + model = llm.model() + print(f"[validator] Auditing with model: {model} via {llm.endpoint_label()}") passed = [] filtered = [] + split_count = 0 for i, turns in enumerate(samples): try: - coherence, quality, reason = _score_sample(client, model, turns) + r = _score_sample(client, model, turns) except Exception as e: print(f"[validator] Sample {i}: scoring failed ({e}), keeping sample.") passed.append(turns) continue - if coherence < COHERENCE_THRESHOLD: - filtered.append((i, "incoherent", coherence, quality, reason)) - elif quality < QUALITY_THRESHOLD: - filtered.append((i, "low-quality", coherence, quality, reason)) + low = ( + r["coherence"] < COHERENCE_THRESHOLD or + r["quality"] < QUALITY_THRESHOLD or + r["pairing"] < PAIRING_THRESHOLD + ) + if r["action"] == "drop" or low: + filtered.append((i, "dropped", r)) + elif r["action"] == "split": + pieces = [p for p in _apply_split(turns, r["split_after"]) if _has_both_roles(p)] + if pieces: + passed.extend(pieces) + split_count += 1 + else: + filtered.append((i, "split-empty", r)) else: passed.append(turns) - print(f"[validator] {len(passed)} passed, {len(filtered)} filtered out of {len(samples)} total.") + print( + f"[validator] {len(passed)} samples kept ({split_count} from splits), " + f"{len(filtered)} dropped, from {len(samples)} input samples." + ) if filtered: - print("[validator] Filtered samples:") - for idx, reason_type, coh, qual, reason in filtered: - print(f" sample {idx:4d} | {reason_type:12s} | coherence={coh:.2f} quality={qual:.2f} | {reason}") + print("[validator] Dropped samples:") + for idx, kind, r in filtered: + print( + f" sample {idx:4d} | {kind:11s} | " + f"coh={r['coherence']:.2f} qual={r['quality']:.2f} pair={r['pairing']:.2f} " + f"| {r['reason']}" + ) return passed diff --git a/requirements.txt b/requirements.txt index 44d86bc..6d21e72 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ llamafactory==0.9.4 # (see https://pytorch.org/get-started/locally/). Installing llamafactory pulls # a torch build, but it may not match your CUDA version. -# Optional — only needed when LLM-based dataset validation is enabled -# (DIALOGSMITH_LLM_VALIDATE / ANTHROPIC_API_KEY). Safe to remove otherwise. -anthropic>=0.39 +# Optional — only needed for the LLM features (quality validation, LLM +# redaction). Uses the OpenAI-compatible API, so it works with OpenAI or any +# local server (Ollama, vLLM, LM Studio, ...). Safe to remove otherwise. +openai>=1.0 diff --git a/setup.bat b/setup.bat index 48449bf..60f78d8 100644 --- a/setup.bat +++ b/setup.bat @@ -19,8 +19,8 @@ if errorlevel 1 (echo Failed to install dependencies. & exit /b 1) echo [3/4] Preparing .env... if not exist ".env" ( - copy ".env.example" ".env" >nul - echo Created .env from .env.example - edit it to enable optional LLM validation. + copy "example.env" ".env" >nul + echo Created .env from example.env - edit it to enable optional LLM features. ) echo [4/4] Processing Telegram export (data\result.json -^> data\chat_sharegpt.json)... diff --git a/setup.sh b/setup.sh index 41ad5bd..e6afab3 100755 --- a/setup.sh +++ b/setup.sh @@ -20,8 +20,8 @@ echo "[2/4] Installing dependencies (this can take a while)..." echo "[3/4] Preparing .env..." if [ ! -f .env ]; then - cp .env.example .env - echo " Created .env from .env.example — edit it to enable optional LLM validation." + cp example.env .env + echo " Created .env from example.env — edit it to enable optional LLM features." fi echo "[4/4] Processing Telegram export (data/result.json -> data/chat_sharegpt.json)..." diff --git a/tests/test_ingest.py b/tests/test_ingest.py index 72b05fb..5fedbab 100644 --- a/tests/test_ingest.py +++ b/tests/test_ingest.py @@ -21,6 +21,7 @@ from ingest import core, sharegpt from ingest.adapters import available_sources, get_adapter from ingest.adapters.telegram import TelegramAdapter +from ingest.message import NormalizedMessage SELF = "Yu Sheng" @@ -148,6 +149,57 @@ def test_gap_splits_conversations(self): self.assertEqual(first_two, EXPECTED_SHAREGPT[:2]) +def _nm(chat, ts, sender, is_self, text, mid=None, reply=None): + return NormalizedMessage( + chat_id=chat, timestamp=ts, sender_id=sender, sender_is_self=is_self, + text=text, message_id=mid, reply_to_id=reply, + ) + + +class ReplyThreadingTest(unittest.TestCase): + def test_reply_stitches_gap_split_conversations(self): + # Two messages an hour+ apart would split into two conversations, but the + # second replies to the first -> they must end up in one sample. + msgs = [ + _nm("c", 1000, "Alice", False, "you free this weekend?", mid="1"), + _nm("c", 1000 + 8000, "Yu", True, "yeah sun works", mid="2", reply="1"), + ] + samples = core.build_samples(msgs) + self.assertEqual(len(samples), 1) + self.assertEqual([t["role"] for t in samples[0]], ["user", "assistant"]) + + def test_no_reply_data_keeps_time_split(self): + # Same timing, no reply link -> still two conversations (one is one-sided + # and dropped), proving threading is a no-op without reply metadata. + msgs = [ + _nm("c", 1000, "Alice", False, "you free this weekend?"), + _nm("c", 1000 + 8000, "Yu", True, "yeah sun works"), + ] + self.assertEqual(core.build_samples(msgs), []) + + +class MultiSpeakerTest(unittest.TestCase): + def _group(self): + return [ + _nm("g", 1, "Bob", False, "q1"), + _nm("g", 2, "Carol", False, "q2"), + _nm("g", 3, "Yu", True, "answer"), + ] + + def test_default_collapses_other_side(self): + out = sharegpt.to_sharegpt(core.build_samples(self._group())) + self.assertEqual(out[0]["conversations"][0], {"from": "human", "value": "q1\nq2"}) + + def test_multi_speaker_labels_users_not_assistant(self): + out = sharegpt.to_sharegpt(core.build_samples(self._group(), multi_speaker=True)) + convs = out[0]["conversations"] + # Distinct speakers stay distinct and are labelled... + self.assertEqual(convs[0], {"from": "human", "value": "Bob: q1"}) + self.assertEqual(convs[1], {"from": "human", "value": "Carol: q2"}) + # ...but the owner's (assistant) turn is never labelled. + self.assertEqual(convs[2], {"from": "gpt", "value": "answer"}) + + class ShareGptTest(unittest.TestCase): def test_role_mapping_and_drop_one_sided(self): samples = [ @@ -167,6 +219,28 @@ def test_jsonl_roundtrip(self): self.assertEqual(sharegpt.load_jsonl_samples(p), samples) +class ValidatorSplitTest(unittest.TestCase): + def test_apply_split_cuts_after_indices(self): + from ingest.validator import _apply_split + turns = [{"role": "user", "text": "a"}, {"role": "assistant", "text": "b"}, + {"role": "user", "text": "c"}, {"role": "assistant", "text": "d"}] + pieces = _apply_split(turns, [1]) + self.assertEqual(len(pieces), 2) + self.assertEqual(pieces[0], turns[:2]) + self.assertEqual(pieces[1], turns[2:]) + + def test_apply_split_ignores_out_of_range(self): + from ingest.validator import _apply_split + turns = [{"role": "user", "text": "a"}, {"role": "assistant", "text": "b"}] + # Index at/after the last turn is meaningless -> no split. + self.assertEqual(_apply_split(turns, [1, 9]), [turns]) + + def test_has_both_roles(self): + from ingest.validator import _has_both_roles + self.assertTrue(_has_both_roles([{"role": "user"}, {"role": "assistant"}])) + self.assertFalse(_has_both_roles([{"role": "user"}, {"role": "user"}])) + + class RegistryTest(unittest.TestCase): def test_telegram_registered(self): self.assertIn("telegram", available_sources()) @@ -180,7 +254,7 @@ def test_unknown_source_raises(self): class CliTest(unittest.TestCase): def test_end_to_end_sharegpt(self): from ingest.cli import main - os.environ["DIALOGSMITH_LLM_VALIDATE"] = "false" # no API calls + os.environ["LLM_VALIDATE"] = "false" # no API calls with tempfile.TemporaryDirectory() as d: inp = _write_fixture(d) out = os.path.join(d, "chat_sharegpt.json") diff --git a/tests/test_redaction.py b/tests/test_redaction.py new file mode 100644 index 0000000..f7415c9 --- /dev/null +++ b/tests/test_redaction.py @@ -0,0 +1,167 @@ +"""Unit tests for regex-based sensitive-data detection (stdlib, no network).""" + +import os +import sys +import types +import unittest + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from ingest import redaction, redactor +from ingest.redaction.sg import nric_valid + + +def _categories(text, locales=None): + return {f.category for f in redaction.scan_text(text, locales)} + + +class UniversalTest(unittest.TestCase): + def test_email(self): + finds = redaction.scan_text("ping me at john.doe@acme.co please") + self.assertEqual([f.category for f in finds], ["EMAIL"]) + self.assertEqual(finds[0].preview, "j***@acme.co") # masked, not raw + + def test_credit_card_luhn(self): + # Valid Visa test number passes; same length with bad checksum does not. + self.assertIn("CARD_NUMBER", _categories("card 4111 1111 1111 1111")) + self.assertNotIn("CARD_NUMBER", _categories("ref 4111 1111 1111 1112")) + + def test_api_keys(self): + self.assertIn("API_KEY", _categories("token sk-abcdefghij0123456789xyz")) + self.assertIn("API_KEY", _categories("AKIAIOSFODNN7EXAMPLE")) + + def test_ipv4(self): + self.assertIn("IP_ADDRESS", _categories("server at 192.168.1.10")) + self.assertNotIn("IP_ADDRESS", _categories("version 999.999.1.1")) + + +class SingaporeTest(unittest.TestCase): + def test_nric_checksum(self): + # S0000001I is a well-formed example; flipping the suffix must fail. + self.assertTrue(nric_valid("S0000001I")) + self.assertFalse(nric_valid("S0000001A")) + + def test_nric_detected_only_when_valid(self): + self.assertIn("NRIC/FIN", _categories("my ic is S0000001I", ["SG"])) + self.assertNotIn("NRIC/FIN", _categories("code S0000001A", ["SG"])) + + def test_nric_case_insensitive(self): + self.assertIn("NRIC/FIN", _categories("ic s0000001i", ["SG"])) + + def test_nric_short_form_requires_context(self): + # With an NRIC/IC keyword nearby it's flagged... + self.assertIn("NRIC/FIN (partial)", _categories("NRIC 123A", ["SG"])) + self.assertIn("NRIC/FIN (partial)", _categories("my IC is 567B", ["SG"])) + # ...but a bare block/unit number is not. + self.assertNotIn("NRIC/FIN (partial)", _categories("Blk 123A Clementi", ["SG"])) + + def test_nric_short_form_reports_only_the_id(self): + finds = [ + f for f in redaction.scan_text("NRIC 123A", ["SG"]) + if f.category == "NRIC/FIN (partial)" + ] + self.assertEqual(finds[0].value, "123A") # keyword excluded from the span + + def test_phone(self): + self.assertIn("PHONE", _categories("call 9123 4567", ["SG"])) + self.assertIn("PHONE", _categories("call +65 9123 4567", ["SG"])) + + def test_locale_filtering(self): + # SG detectors don't run when only universal locale is requested. + self.assertNotIn("NRIC/FIN", _categories("ic S0000001I", [])) + + +class RegistryTest(unittest.TestCase): + def test_no_duplicate_names(self): + names = [d.name for d in redaction.iter_detectors()] + self.assertEqual(len(names), len(set(names))) + + def test_locales_available(self): + self.assertIn("SG", redaction.available_locales()) + self.assertIn("universal", redaction.available_locales()) + + +class RedactorStageTest(unittest.TestCase): + def _samples(self): + return [ + [{"role": "user", "text": "email me at a@b.com"}, + {"role": "assistant", "text": "sure thing"}], + [{"role": "user", "text": "nothing sensitive here"}, + {"role": "assistant", "text": "ok"}], + ] + + def test_scan_is_nondestructive_and_reports(self): + samples = self._samples() + report = redactor.scan_samples(samples) + self.assertEqual(report["total_findings"], 1) + self.assertIn("EMAIL", report["summary"]) + # Original samples untouched. + self.assertEqual(samples[0][0]["text"], "email me at a@b.com") + + def test_apply_replace_uses_placeholder(self): + out = redactor.apply(self._samples(), "replace") + self.assertEqual(out[0][0]["text"], "email me at [EMAIL]") + self.assertEqual(out[1][0]["text"], "nothing sensitive here") # untouched + + def test_apply_drop_removes_conversation(self): + out = redactor.apply(self._samples(), "drop") + self.assertEqual(len(out), 1) # the one with an email is dropped + self.assertEqual(out[0][0]["text"], "nothing sensitive here") + + +class _FakeClient: + """Stub OpenAI-compatible client returning a canned JSON body (no network).""" + + def __init__(self, text): + message = types.SimpleNamespace(content=text) + resp = types.SimpleNamespace(choices=[types.SimpleNamespace(message=message)]) + completions = types.SimpleNamespace(create=lambda **kw: resp) + self.chat = types.SimpleNamespace(completions=completions) + + +class LlmRedactionTest(unittest.TestCase): + def _samples(self): + return [[{"role": "user", "text": "hi I'm Alice from Acme"}, + {"role": "assistant", "text": "hello"}]] + + def test_verbatim_span_is_located_and_masked(self): + client = _FakeClient( + '{"findings":[{"turn":0,"text":"Alice","category":"NAME","severity":"high"}]}' + ) + finds = redactor.llm_scan_samples(self._samples(), client, "model") + self.assertEqual(len(finds), 1) + self.assertEqual(finds[0]["category"], "NAME") + self.assertEqual(finds[0]["start"], 7) # offset of "Alice" + self.assertEqual(finds[0]["end"], 12) + self.assertNotIn("Alice", finds[0]["preview"]) # masked + + def test_unlocatable_span_is_dropped(self): + # Model paraphrased instead of copying -> can't verify -> skipped. + client = _FakeClient( + '{"findings":[{"turn":0,"text":"Bob","category":"NAME","severity":"high"}]}' + ) + self.assertEqual(redactor.llm_scan_samples(self._samples(), client, "model"), []) + + def test_merge_into_report(self): + report = redactor.scan_samples(self._samples()) # 0 regex findings + llm = [{"conversation": 0, "turn": 0, "role": "user", "category": "NAME", + "severity": "high", "start": 6, "end": 11, "preview": "Al**e"}] + redactor.merge_llm_findings(report, llm) + self.assertEqual(report["total_findings"], 1) + self.assertIn("NAME", report["summary"]) + self.assertNotIn("value", report["findings"][0]) # no raw span persisted + + def test_apply_replace_uses_llm_offsets(self): + llm = [{"conversation": 0, "turn": 0, "category": "NAME", + "start": 7, "end": 12}] + out = redactor.apply(self._samples(), "replace", llm_findings=llm) + self.assertEqual(out[0][0]["text"], "hi I'm [NAME] from Acme") + + def test_replace_spans_drops_overlap(self): + from ingest.redactor import _replace_spans + # Two overlapping spans -> only the right-most is applied. + self.assertEqual(_replace_spans("abcdef", [(0, 3, "X"), (2, 5, "Y")]), "ab[Y]f") + + +if __name__ == "__main__": + unittest.main() From a9367c7f1b520cfe27b668b5b9acd8dc9315013e Mon Sep 17 00:00:00 2001 From: NotYuSheng Date: Tue, 23 Jun 2026 22:52:47 +0800 Subject: [PATCH 02/15] chore: remove deprecated script shims; fix remote name in docs The scripts/telegram_extract.py and scripts/convert_to_sharegpt.py shims only delegated to `python -m ingest`; remove them and update the Legacy Workflow note. Co-Authored-By: Claude Opus 4.8 --- README.md | 2 +- scripts/convert_to_sharegpt.py | 29 ----------------------------- scripts/telegram_extract.py | 28 ---------------------------- 3 files changed, 1 insertion(+), 58 deletions(-) delete mode 100644 scripts/convert_to_sharegpt.py delete mode 100644 scripts/telegram_extract.py diff --git a/README.md b/README.md index 8cf44a7..b635033 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,7 @@ It runs in well under a second and locks in the conversion behaviour, so you can ## Legacy Workflow -The pre-refactor, Windows-only workflow (which cloned LLaMA-Factory at HEAD) is preserved at the [`v0.1.0`](https://github.com/NotYuSheng/Doppelganger/releases/tag/v0.1.0) tag. The old `scripts/telegram_extract.py` and `scripts/convert_to_sharegpt.py` still work as thin deprecated wrappers around `python -m ingest`, but will be removed in a future release. +The pre-refactor, Windows-only workflow (which cloned LLaMA-Factory at HEAD) is preserved at the [`v0.1.0`](https://github.com/NotYuSheng/Doppelganger/releases/tag/v0.1.0) tag. The old `scripts/telegram_extract.py` and `scripts/convert_to_sharegpt.py` shims have been removed — use `python -m ingest` instead. ## Star History diff --git a/scripts/convert_to_sharegpt.py b/scripts/convert_to_sharegpt.py deleted file mode 100644 index 7d99723..0000000 --- a/scripts/convert_to_sharegpt.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 -"""DEPRECATED shim — kept for backwards compatibility, removed in a future release. - -The new pipeline writes ShareGPT directly: - - python -m ingest --source telegram --format sharegpt - -This shim still converts an existing data/chat_dataset.jsonl into -data/chat_sharegpt.json, delegating to the new ``ingest`` package. -""" - -import os -import sys - -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from ingest import sharegpt # noqa: E402 - -INPUT_PATH = "./data/chat_dataset.jsonl" -OUTPUT_PATH = "./data/chat_sharegpt.json" - -if __name__ == "__main__": - sys.stderr.write( - "[deprecated] scripts/convert_to_sharegpt.py -> use: " - "python -m ingest --source telegram --format sharegpt\n" - ) - samples = sharegpt.load_jsonl_samples(INPUT_PATH) - written = sharegpt.write_sharegpt(samples, OUTPUT_PATH) - print(f"Converted {written} valid conversation samples to ShareGPT format.") diff --git a/scripts/telegram_extract.py b/scripts/telegram_extract.py deleted file mode 100644 index 548d004..0000000 --- a/scripts/telegram_extract.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -"""DEPRECATED shim — kept for backwards compatibility, removed in a future release. - -Use the cross-platform CLI instead: - - python -m ingest --source telegram --format jsonl - -This shim reproduces the old behaviour (Telegram result.json -> -data/chat_dataset.jsonl) by delegating to the new ``ingest`` package. -""" - -import os -import sys - -# Allow running as `python scripts/telegram_extract.py` from the repo root. -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from ingest.cli import main # noqa: E402 - -if __name__ == "__main__": - sys.stderr.write( - "[deprecated] scripts/telegram_extract.py -> use: " - "python -m ingest --source telegram --format jsonl\n" - ) - raise SystemExit( - main(["--source", "telegram", "--format", "jsonl", - "--output", "./data/chat_dataset.jsonl"]) - ) From 33a5cd5b77111b7a2bb236ea002ab7a8c459cb92 Mon Sep 17 00:00:00 2001 From: NotYuSheng Date: Tue, 23 Jun 2026 22:56:16 +0800 Subject: [PATCH 03/15] docs: resolve conflicting consent wording in top warnings Caution block now owns data-sensitivity + consent + law; Important block owns model-misuse. Removes the contradictory 'never consented' phrasing and the duplicate consent line. Co-Authored-By: Claude Opus 4.8 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b635033..d6f8f8d 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,10 @@ Doppelganger fine-tunes large language models (like Qwen) on your own chat conve Ingestion is **source-agnostic**: a small adapter parses each platform's export into a normalized message stream, and the rest of the pipeline (sessionizing, turn-merging, sensitive-data scanning, optional quality auditing, ShareGPT formatting) is shared. **Telegram** is supported today, with **WhatsApp**, **Discord**, and other platforms planned — each slots in as a drop-in adapter. > [!CAUTION] -> **Your chat history is sensitive data, and you are responsible for it.** A model fine-tuned on it can memorize and later reproduce personal identifiers, private conversations, credentials, and things said by other people who never consented. The built-in [sensitive-data scanning](#privacy--sensitive-data) is a **safety net, not a guarantee** — both regex and LLM detection miss real cases and raise false positives. Before training, sharing, or deploying anything, **review the dataset yourself**, obtain any consent you need, and ensure you comply with applicable privacy laws. Treat trained adapters and merged checkpoints as sensitive too — they can leak the data they were trained on. +> **Your chat history is sensitive data, and you are responsible for it.** A model fine-tuned on it can memorize and later reproduce personal identifiers, private conversations, credentials, and messages written by other people in your chats. The built-in [sensitive-data scanning](#privacy--sensitive-data) is a **safety net, not a guarantee** — both regex and LLM detection miss real cases and raise false positives. Before training, sharing, or deploying anything: **review the dataset yourself**, get consent from others whose messages are included (especially in group chats), and comply with applicable privacy laws. Treat trained adapters and merged checkpoints as sensitive too — they can leak the data they were trained on. > [!IMPORTANT] -> **This is a for-fun, experimental project — not a production tool.** A model that imitates a real person can be misused for impersonation, deception, or social engineering, and it will happily generate convincing messages that person never actually wrote. Don't present its output as genuinely from anyone, don't train on someone else's chats without their knowledge, and don't rely on it for anything that matters. Enjoy it responsibly. +> **This is a for-fun, experimental project — not a production tool.** A model that imitates a real person can be misused for impersonation, deception, or social engineering, and it will happily generate convincing messages that person never actually wrote. Don't present its output as genuinely from anyone, and don't rely on it for anything that matters. Enjoy it responsibly. Fine-tuning on your chats can capture your: From 7c143fe44f78b168a414f203701a4a4363846b10 Mon Sep 17 00:00:00 2001 From: NotYuSheng Date: Tue, 23 Jun 2026 23:05:44 +0800 Subject: [PATCH 04/15] docs: add Roadmap section (training techniques + tracked issues) States the project's intent to explore pre-training, fine-tuning, and alignment, plus persona prompting (#14), more adapters (#9), NER redaction (#13), and wider locale packs (#15). Co-Authored-By: Claude Opus 4.8 --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index d6f8f8d..194315c 100644 --- a/README.md +++ b/README.md @@ -296,6 +296,18 @@ It runs in well under a second and locks in the conversion behaviour, so you can The pre-refactor, Windows-only workflow (which cloned LLaMA-Factory at HEAD) is preserved at the [`v0.1.0`](https://github.com/NotYuSheng/Doppelganger/releases/tag/v0.1.0) tag. The old `scripts/telegram_extract.py` and `scripts/convert_to_sharegpt.py` shims have been removed — use `python -m ingest` instead. +## Roadmap + +Capturing how someone communicates is bigger than any single method. Today Doppelganger uses **LoRA fine-tuning (SFT)** on your chats; the project intends to explore other training techniques and context sources over time: + +- **More training techniques** — beyond fine-tuning, e.g. continued **pre-training** on larger personal corpora and **alignment / preference tuning** (DPO) to refine behaviour. +- **Persona prompting** — a short quiz that generates a system prompt for explicit preferences/facts, complementing the fine-tuned *style* ([#14](https://github.com/NotYuSheng/Doppelganger/issues/14)). +- **More chat sources** — WhatsApp, Discord, and others as drop-in adapters ([#9](https://github.com/NotYuSheng/Doppelganger/issues/9)). +- **Offline NER redaction** — name/location detection without an LLM ([#13](https://github.com/NotYuSheng/Doppelganger/issues/13)). +- **Wider locale coverage** — more country detector packs ([#15](https://github.com/NotYuSheng/Doppelganger/issues/15) tracks the Singapore gaps). + +This is an experimental, for-fun project — the roadmap is exploratory, not a commitment. + ## Star History From 4e9cb917d21bff1c20977bd4e4a3e929093eaccb Mon Sep 17 00:00:00 2001 From: NotYuSheng Date: Tue, 23 Jun 2026 23:08:35 +0800 Subject: [PATCH 05/15] docs: expand Roadmap into exploration vision across AI techniques Frames the project as a learning sandbox spanning training, memory/RAG, agents, MCP, guardrails, and evaluation. Links the new exploration issues (#16-#21) alongside existing tracked work (#9, #13, #14, #15). Co-Authored-By: Claude Opus 4.8 --- README.md | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 194315c..54fae35 100644 --- a/README.md +++ b/README.md @@ -296,17 +296,33 @@ It runs in well under a second and locks in the conversion behaviour, so you can The pre-refactor, Windows-only workflow (which cloned LLaMA-Factory at HEAD) is preserved at the [`v0.1.0`](https://github.com/NotYuSheng/Doppelganger/releases/tag/v0.1.0) tag. The old `scripts/telegram_extract.py` and `scripts/convert_to_sharegpt.py` shims have been removed — use `python -m ingest` instead. -## Roadmap +## Roadmap & Vision -Capturing how someone communicates is bigger than any single method. Today Doppelganger uses **LoRA fine-tuning (SFT)** on your chats; the project intends to explore other training techniques and context sources over time: +Doppelganger is as much a **learning sandbox** as a tool: the aim is to explore the *full* AI toolbox for capturing how a person communicates, and to find what actually moves the needle on *"does this sound like me?"*. Today that's LoRA fine-tuning — everything below is exploratory. -- **More training techniques** — beyond fine-tuning, e.g. continued **pre-training** on larger personal corpora and **alignment / preference tuning** (DPO) to refine behaviour. -- **Persona prompting** — a short quiz that generates a system prompt for explicit preferences/facts, complementing the fine-tuned *style* ([#14](https://github.com/NotYuSheng/Doppelganger/issues/14)). -- **More chat sources** — WhatsApp, Discord, and others as drop-in adapters ([#9](https://github.com/NotYuSheng/Doppelganger/issues/9)). -- **Offline NER redaction** — name/location detection without an LLM ([#13](https://github.com/NotYuSheng/Doppelganger/issues/13)). -- **Wider locale coverage** — more country detector packs ([#15](https://github.com/NotYuSheng/Doppelganger/issues/15) tracks the Singapore gaps). +**Shaping the model** +- Training techniques — **pre-training**, **fine-tuning** (today), and **alignment / preference tuning** (DPO). +- **Continual learning** — keep the model current as new chats arrive, without catastrophic forgetting ([#18](https://github.com/NotYuSheng/Doppelganger/issues/18)). -This is an experimental, for-fun project — the roadmap is exploratory, not a commitment. +**Giving it context & memory** +- **RAG** — retrieve your past messages and memories at inference instead of baking everything into weights ([#16](https://github.com/NotYuSheng/Doppelganger/issues/16)). +- **Persona prompting** — a quiz that generates a system prompt for explicit facts/preferences, complementing the fine-tuned *style* ([#14](https://github.com/NotYuSheng/Doppelganger/issues/14)). +- **MCP** — expose memory/tools (or the doppelganger itself) via the Model Context Protocol ([#20](https://github.com/NotYuSheng/Doppelganger/issues/20)). + +**Making it act** +- **Agentic doppelganger** — tool use and bounded actions on your behalf ([#19](https://github.com/NotYuSheng/Doppelganger/issues/19)). + +**Keeping it safe & honest** +- **Guardrails** — block harmful output and sensitive-data leakage in generations ([#21](https://github.com/NotYuSheng/Doppelganger/issues/21)). +- Sensitive-data redaction (shipped) + **offline NER** for names/locations ([#13](https://github.com/NotYuSheng/Doppelganger/issues/13)). + +**Knowing if it works** +- **Evaluation** — measure style fidelity: LLM-as-judge, held-out perplexity, stylometrics, blind human A/B ([#17](https://github.com/NotYuSheng/Doppelganger/issues/17)). + +**More data & coverage** +- More chat sources — WhatsApp, Discord, and others as drop-in adapters ([#9](https://github.com/NotYuSheng/Doppelganger/issues/9)); wider locale detector packs ([#15](https://github.com/NotYuSheng/Doppelganger/issues/15)). + +> This is an experimental, for-fun project — the roadmap is a wishlist of things to explore, not a commitment. ## Star History From 4c207ea89e06352172302f6f63a21268cd12a087 Mon Sep 17 00:00:00 2001 From: NotYuSheng Date: Tue, 23 Jun 2026 23:13:06 +0800 Subject: [PATCH 06/15] docs: expand Roadmap to full exploration backlog (#16-#38) Adds multimodal, inference-time control, and interpretability tracks and links the complete exploration backlog; points to the exploration label for the rest. Co-Authored-By: Claude Opus 4.8 --- README.md | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 54fae35..cc6d1f6 100644 --- a/README.md +++ b/README.md @@ -298,29 +298,23 @@ The pre-refactor, Windows-only workflow (which cloned LLaMA-Factory at HEAD) is ## Roadmap & Vision -Doppelganger is as much a **learning sandbox** as a tool: the aim is to explore the *full* AI toolbox for capturing how a person communicates, and to find what actually moves the needle on *"does this sound like me?"*. Today that's LoRA fine-tuning — everything below is exploratory. +Doppelganger is as much a **learning sandbox** as a tool: the aim is to explore the *full* AI toolbox for capturing how a person communicates, and to find what actually moves the needle on *"does this sound like me?"*. Today that's LoRA fine-tuning — everything below is exploratory. The full backlog lives under the [`exploration`](https://github.com/NotYuSheng/Doppelganger/issues?q=is%3Aissue+is%3Aopen+label%3Aexploration) label. -**Shaping the model** -- Training techniques — **pre-training**, **fine-tuning** (today), and **alignment / preference tuning** (DPO). -- **Continual learning** — keep the model current as new chats arrive, without catastrophic forgetting ([#18](https://github.com/NotYuSheng/Doppelganger/issues/18)). +**Shaping the model** — pre-training · fine-tuning (today) · alignment/DPO · continual learning ([#18](https://github.com/NotYuSheng/Doppelganger/issues/18)) · synthetic data / self-instruct ([#22](https://github.com/NotYuSheng/Doppelganger/issues/22)) · multi-LoRA personas & merging ([#23](https://github.com/NotYuSheng/Doppelganger/issues/23)) · distillation to on-device ([#24](https://github.com/NotYuSheng/Doppelganger/issues/24)) · PEFT comparison ([#25](https://github.com/NotYuSheng/Doppelganger/issues/25)) -**Giving it context & memory** -- **RAG** — retrieve your past messages and memories at inference instead of baking everything into weights ([#16](https://github.com/NotYuSheng/Doppelganger/issues/16)). -- **Persona prompting** — a quiz that generates a system prompt for explicit facts/preferences, complementing the fine-tuned *style* ([#14](https://github.com/NotYuSheng/Doppelganger/issues/14)). -- **MCP** — expose memory/tools (or the doppelganger itself) via the Model Context Protocol ([#20](https://github.com/NotYuSheng/Doppelganger/issues/20)). +**Giving it context & memory** — RAG ([#16](https://github.com/NotYuSheng/Doppelganger/issues/16)) · long-term memory + reflection ([#26](https://github.com/NotYuSheng/Doppelganger/issues/26)) · relationship/knowledge graph ([#27](https://github.com/NotYuSheng/Doppelganger/issues/27)) · style embeddings / user-conditioning ([#28](https://github.com/NotYuSheng/Doppelganger/issues/28)) · persona-prompt quiz ([#14](https://github.com/NotYuSheng/Doppelganger/issues/14)) · MCP ([#20](https://github.com/NotYuSheng/Doppelganger/issues/20)) -**Making it act** -- **Agentic doppelganger** — tool use and bounded actions on your behalf ([#19](https://github.com/NotYuSheng/Doppelganger/issues/19)). +**Multimodal** — voice cloning, TTS/STT ([#29](https://github.com/NotYuSheng/Doppelganger/issues/29)) · stickers / emoji / memes ([#30](https://github.com/NotYuSheng/Doppelganger/issues/30)) -**Keeping it safe & honest** -- **Guardrails** — block harmful output and sensitive-data leakage in generations ([#21](https://github.com/NotYuSheng/Doppelganger/issues/21)). -- Sensitive-data redaction (shipped) + **offline NER** for names/locations ([#13](https://github.com/NotYuSheng/Doppelganger/issues/13)). +**Making it act** — agentic doppelganger ([#19](https://github.com/NotYuSheng/Doppelganger/issues/19)) · multi-agent & self-play ([#31](https://github.com/NotYuSheng/Doppelganger/issues/31)) · proactive / initiative modeling ([#32](https://github.com/NotYuSheng/Doppelganger/issues/32)) -**Knowing if it works** -- **Evaluation** — measure style fidelity: LLM-as-judge, held-out perplexity, stylometrics, blind human A/B ([#17](https://github.com/NotYuSheng/Doppelganger/issues/17)). +**Inference-time control** — activation steering / control vectors ([#36](https://github.com/NotYuSheng/Doppelganger/issues/36)) · prompt optimization (DSPy) ([#37](https://github.com/NotYuSheng/Doppelganger/issues/37)) -**More data & coverage** -- More chat sources — WhatsApp, Discord, and others as drop-in adapters ([#9](https://github.com/NotYuSheng/Doppelganger/issues/9)); wider locale detector packs ([#15](https://github.com/NotYuSheng/Doppelganger/issues/15)). +**Keeping it safe & honest** — guardrails ([#21](https://github.com/NotYuSheng/Doppelganger/issues/21)) · redaction (shipped) + offline NER ([#13](https://github.com/NotYuSheng/Doppelganger/issues/13)) · differential-privacy training ([#33](https://github.com/NotYuSheng/Doppelganger/issues/33)) · machine unlearning ([#34](https://github.com/NotYuSheng/Doppelganger/issues/34)) · memorization audits / canaries / watermarking / federated ([#35](https://github.com/NotYuSheng/Doppelganger/issues/35)) + +**Knowing if it works** — evaluation, "does it sound like me?" ([#17](https://github.com/NotYuSheng/Doppelganger/issues/17)) · interpretability, "what did it learn about me?" ([#38](https://github.com/NotYuSheng/Doppelganger/issues/38)) + +**More data & coverage** — more chat sources: WhatsApp, Discord, … ([#9](https://github.com/NotYuSheng/Doppelganger/issues/9)) · wider locale detector packs ([#15](https://github.com/NotYuSheng/Doppelganger/issues/15)) > This is an experimental, for-fun project — the roadmap is a wishlist of things to explore, not a commitment. From 23b12c5c7aac74d0d1136cdbb9ea62516f200d66 Mon Sep 17 00:00:00 2001 From: NotYuSheng Date: Tue, 23 Jun 2026 23:14:26 +0800 Subject: [PATCH 07/15] docs: state roadmap techniques without issue links Keep the README roadmap as a plain statement of exploration areas; the issue tracker holds the live backlog. Co-Authored-By: Claude Opus 4.8 --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index cc6d1f6..8208ea5 100644 --- a/README.md +++ b/README.md @@ -298,23 +298,23 @@ The pre-refactor, Windows-only workflow (which cloned LLaMA-Factory at HEAD) is ## Roadmap & Vision -Doppelganger is as much a **learning sandbox** as a tool: the aim is to explore the *full* AI toolbox for capturing how a person communicates, and to find what actually moves the needle on *"does this sound like me?"*. Today that's LoRA fine-tuning — everything below is exploratory. The full backlog lives under the [`exploration`](https://github.com/NotYuSheng/Doppelganger/issues?q=is%3Aissue+is%3Aopen+label%3Aexploration) label. +Doppelganger is as much a **learning sandbox** as a tool: the aim is to explore the *full* AI toolbox for capturing how a person communicates, and to find what actually moves the needle on *"does this sound like me?"*. Today that's LoRA fine-tuning — everything below is exploratory (see the issue tracker for the live backlog). -**Shaping the model** — pre-training · fine-tuning (today) · alignment/DPO · continual learning ([#18](https://github.com/NotYuSheng/Doppelganger/issues/18)) · synthetic data / self-instruct ([#22](https://github.com/NotYuSheng/Doppelganger/issues/22)) · multi-LoRA personas & merging ([#23](https://github.com/NotYuSheng/Doppelganger/issues/23)) · distillation to on-device ([#24](https://github.com/NotYuSheng/Doppelganger/issues/24)) · PEFT comparison ([#25](https://github.com/NotYuSheng/Doppelganger/issues/25)) +**Shaping the model** — pre-training · fine-tuning (today) · alignment/DPO · continual learning · synthetic data / self-instruct · multi-LoRA personas & merging · distillation to on-device · PEFT comparison -**Giving it context & memory** — RAG ([#16](https://github.com/NotYuSheng/Doppelganger/issues/16)) · long-term memory + reflection ([#26](https://github.com/NotYuSheng/Doppelganger/issues/26)) · relationship/knowledge graph ([#27](https://github.com/NotYuSheng/Doppelganger/issues/27)) · style embeddings / user-conditioning ([#28](https://github.com/NotYuSheng/Doppelganger/issues/28)) · persona-prompt quiz ([#14](https://github.com/NotYuSheng/Doppelganger/issues/14)) · MCP ([#20](https://github.com/NotYuSheng/Doppelganger/issues/20)) +**Giving it context & memory** — RAG · long-term memory + reflection · relationship/knowledge graph · style embeddings / user-conditioning · persona-prompt quiz · MCP -**Multimodal** — voice cloning, TTS/STT ([#29](https://github.com/NotYuSheng/Doppelganger/issues/29)) · stickers / emoji / memes ([#30](https://github.com/NotYuSheng/Doppelganger/issues/30)) +**Multimodal** — voice cloning, TTS/STT · stickers / emoji / memes -**Making it act** — agentic doppelganger ([#19](https://github.com/NotYuSheng/Doppelganger/issues/19)) · multi-agent & self-play ([#31](https://github.com/NotYuSheng/Doppelganger/issues/31)) · proactive / initiative modeling ([#32](https://github.com/NotYuSheng/Doppelganger/issues/32)) +**Making it act** — agentic doppelganger · multi-agent & self-play · proactive / initiative modeling -**Inference-time control** — activation steering / control vectors ([#36](https://github.com/NotYuSheng/Doppelganger/issues/36)) · prompt optimization (DSPy) ([#37](https://github.com/NotYuSheng/Doppelganger/issues/37)) +**Inference-time control** — activation steering / control vectors · prompt optimization (DSPy) -**Keeping it safe & honest** — guardrails ([#21](https://github.com/NotYuSheng/Doppelganger/issues/21)) · redaction (shipped) + offline NER ([#13](https://github.com/NotYuSheng/Doppelganger/issues/13)) · differential-privacy training ([#33](https://github.com/NotYuSheng/Doppelganger/issues/33)) · machine unlearning ([#34](https://github.com/NotYuSheng/Doppelganger/issues/34)) · memorization audits / canaries / watermarking / federated ([#35](https://github.com/NotYuSheng/Doppelganger/issues/35)) +**Keeping it safe & honest** — guardrails · redaction (shipped) + offline NER · differential-privacy training · machine unlearning · memorization audits / canaries / watermarking / federated -**Knowing if it works** — evaluation, "does it sound like me?" ([#17](https://github.com/NotYuSheng/Doppelganger/issues/17)) · interpretability, "what did it learn about me?" ([#38](https://github.com/NotYuSheng/Doppelganger/issues/38)) +**Knowing if it works** — evaluation, "does it sound like me?" · interpretability, "what did it learn about me?" -**More data & coverage** — more chat sources: WhatsApp, Discord, … ([#9](https://github.com/NotYuSheng/Doppelganger/issues/9)) · wider locale detector packs ([#15](https://github.com/NotYuSheng/Doppelganger/issues/15)) +**More data & coverage** — more chat sources: WhatsApp, Discord, … · wider locale detector packs > This is an experimental, for-fun project — the roadmap is a wishlist of things to explore, not a commitment. From e5b8d9cc16f37a4a7d3493d54c793d039df10496 Mon Sep 17 00:00:00 2001 From: NotYuSheng Date: Tue, 23 Jun 2026 23:30:01 +0800 Subject: [PATCH 08/15] fix: address PR review (span overlap, speaker prefix, LLM failure aborts, SG postal) - redactor._replace_spans: on overlap keep the outer/longer span so an inner span can't leak the rest (e.g. DOMAIN inside EMAIL) [security-high]. - core._assemble_turns: apply the speaker prefix only once per merged turn. - redactor.llm_scan_samples + validator.validate_samples: abort after 5 consecutive LLM failures instead of flooding the console. - redaction/sg.py: sg_postal also matches the common "S123456" form. Co-Authored-By: Claude Opus 4.8 --- ingest/core.py | 5 +++-- ingest/redaction/sg.py | 4 ++-- ingest/redactor.py | 23 ++++++++++++++++------- ingest/validator.py | 8 ++++++++ tests/test_redaction.py | 12 +++++++++--- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/ingest/core.py b/ingest/core.py index c27b6fe..81c2e02 100644 --- a/ingest/core.py +++ b/ingest/core.py @@ -154,7 +154,6 @@ def _assemble_turns(raw_turns, multi_speaker: bool) -> Sample: for sender_id, is_self, text in raw_turns: role = "assistant" if is_self else "user" - value = f"{sender_id}: {text}" if (multi_speaker and role == "user") else text same_role = bool(turns) and turns[-1]["role"] == role # In multi-speaker mode a user turn only merges with the previous turn @@ -163,8 +162,10 @@ def _assemble_turns(raw_turns, multi_speaker: bool) -> Sample: multi_speaker and role == "user" and last_sender != sender_id ) if mergeable: - turns[-1]["text"] += "\n" + value + # Continuation of the same turn — don't repeat the speaker prefix. + turns[-1]["text"] += "\n" + text else: + value = f"{sender_id}: {text}" if (multi_speaker and role == "user") else text turns.append({"role": role, "text": value}) last_sender = sender_id diff --git a/ingest/redaction/sg.py b/ingest/redaction/sg.py index 7b57cdb..b78a391 100644 --- a/ingest/redaction/sg.py +++ b/ingest/redaction/sg.py @@ -82,11 +82,11 @@ def nric_valid(value: str) -> bool: ) # Postal code is 6 bare digits — far too noisy alone, so require an explicit -# "Singapore " or "S(code)" context to keep precision up. +# "Singapore ", "S123456", or "S(123456)" context to keep precision up. make( "sg_postal", "POSTAL_CODE", "SG", - r"(?:[Ss]ingapore\s+|\bS\()\d{6}\)?", + r"(?:[Ss]ingapore\s+|\bS\(?)\d{6}\)?", severity="low", ) diff --git a/ingest/redactor.py b/ingest/redactor.py index 39786fa..85edec5 100644 --- a/ingest/redactor.py +++ b/ingest/redactor.py @@ -21,6 +21,7 @@ from ingest import redaction DEFAULT_LOCALES = ["SG"] # universal detectors always run in addition to these +_MAX_CONSECUTIVE_LLM_FAILURES = 5 # abort the LLM pass if the endpoint keeps failing def scan_samples(samples, locales: Optional[Iterable[str]] = None) -> dict: @@ -92,16 +93,18 @@ def print_summary(report: dict, report_path: str, mode: str = "off") -> None: def _replace_spans(text: str, spans) -> str: """Replace ``(start, end, category)`` spans with ``[CATEGORY]`` placeholders. - Drops overlapping spans (keeping the right-most) and replaces right-to-left - so each replacement leaves earlier offsets valid. + On overlap, keep the longer/outermost span — so an inner ``DOMAIN`` can't + survive while its enclosing ``EMAIL`` is dropped, which would leave the email + username exposed. Sort by start ascending then end descending, greedily keep + non-overlapping spans, and apply right-to-left so earlier offsets stay valid. """ chosen = [] - boundary = len(text) + 1 # left edge of the span accepted to our right - for start, end, cat in sorted(set(spans), key=lambda s: -s[0]): - if end <= boundary: + last_end = 0 + for start, end, cat in sorted(set(spans), key=lambda s: (s[0], -s[1])): + if start >= last_end: chosen.append((start, end, cat)) - boundary = start - for start, end, cat in chosen: # already right-to-left + last_end = end + for start, end, cat in reversed(chosen): text = text[:start] + f"[{cat}]" + text[end:] return text @@ -195,11 +198,17 @@ def llm_scan_samples(samples, client, model) -> List[dict]: rather than trusting an offset we can't confirm. """ findings = [] + consecutive_failures = 0 for ci, turns in enumerate(samples): try: raw = _llm_audit_conversation(client, model, turns) + consecutive_failures = 0 except Exception as e: + consecutive_failures += 1 print(f"[redactor] LLM scan failed on conversation {ci}: {e}") + if consecutive_failures >= _MAX_CONSECUTIVE_LLM_FAILURES: + print("[redactor] Too many consecutive LLM failures — aborting LLM scan.") + break continue for rf in raw: try: diff --git a/ingest/validator.py b/ingest/validator.py index 7a684c2..103ebed 100644 --- a/ingest/validator.py +++ b/ingest/validator.py @@ -25,6 +25,7 @@ from ingest import llm +_MAX_CONSECUTIVE_LLM_FAILURES = 5 # abort validation if the endpoint keeps failing COHERENCE_THRESHOLD = 0.5 # below this the conversation is considered incoherent QUALITY_THRESHOLD = 0.5 # below this the sample is considered low-quality PAIRING_THRESHOLD = 0.5 # below this the turns don't respond to each other @@ -135,13 +136,20 @@ def validate_samples(samples): passed = [] filtered = [] split_count = 0 + consecutive_failures = 0 for i, turns in enumerate(samples): try: r = _score_sample(client, model, turns) + consecutive_failures = 0 except Exception as e: + consecutive_failures += 1 print(f"[validator] Sample {i}: scoring failed ({e}), keeping sample.") passed.append(turns) + if consecutive_failures >= _MAX_CONSECUTIVE_LLM_FAILURES: + print("[validator] Too many consecutive LLM failures — keeping remaining samples unvalidated.") + passed.extend(samples[i + 1:]) + break continue low = ( diff --git a/tests/test_redaction.py b/tests/test_redaction.py index f7415c9..7630df8 100644 --- a/tests/test_redaction.py +++ b/tests/test_redaction.py @@ -157,10 +157,16 @@ def test_apply_replace_uses_llm_offsets(self): out = redactor.apply(self._samples(), "replace", llm_findings=llm) self.assertEqual(out[0][0]["text"], "hi I'm [NAME] from Acme") - def test_replace_spans_drops_overlap(self): + def test_replace_spans_prefers_outer_span(self): from ingest.redactor import _replace_spans - # Two overlapping spans -> only the right-most is applied. - self.assertEqual(_replace_spans("abcdef", [(0, 3, "X"), (2, 5, "Y")]), "ab[Y]f") + # Partial overlap -> keep the earlier/outer span, one clean replacement. + self.assertEqual(_replace_spans("abcdef", [(0, 3, "X"), (2, 5, "Y")]), "[X]def") + # Nested: the inner span must not survive while its enclosing span is + # dropped (which would leave the uncovered prefix exposed). + self.assertEqual( + _replace_spans("a@b.com x", [(0, 7, "EMAIL"), (2, 7, "DOMAIN")]), + "[EMAIL] x", + ) if __name__ == "__main__": From 9619e7f909bba097b7692dbf77ddebc792c61eae Mon Sep 17 00:00:00 2001 From: NotYuSheng Date: Tue, 23 Jun 2026 23:30:01 +0800 Subject: [PATCH 09/15] refactor: local-first LLM config (no hardcoded model default, drop cloud suggestions) - llm.py: remove DEFAULT_MODEL; LLM_MODEL is required to enable the LLM features (clear error otherwise). Docs use the vLLM/LM Studio HF model-id convention. - example.env / README: lead with a LOCAL OpenAI-compatible server (Qwen/Qwen2.5-7B-Instruct), remove gpt-4o-mini/cloud suggestions; drop the roadmap "(today)" qualifier. Co-Authored-By: Claude Opus 4.8 --- README.md | 16 +++++++--------- example.env | 33 ++++++++++++--------------------- ingest/llm.py | 32 ++++++++++++++++++-------------- 3 files changed, 37 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 8208ea5..33e1067 100644 --- a/README.md +++ b/README.md @@ -103,15 +103,13 @@ python -m ingest --source telegram **3. (Optional) Configure LLM features** -Copy `example.env` to `.env` (the setup scripts do this for you) and fill it in to enable the quality auditor and LLM redaction. Local endpoints keep your chat data on your machine: +The core pipeline needs no LLM. To *also* enable the quality auditor and LLM redaction, copy `example.env` to `.env` (the setup scripts do this) and point it at a **local** OpenAI-compatible server (vLLM, LM Studio, llama.cpp) so your chat data stays on your machine: ```dotenv LLM_VALIDATE=true -LLM_MODEL=gpt-4o-mini -LLM_API_KEY=your_api_key_here -# For a local model instead (key can be any value): -# LLM_API_BASE_URL=http://localhost:11434/v1 -# LLM_MODEL=qwen2.5 +LLM_API_BASE_URL=http://localhost:8000/v1 # vLLM (LM Studio uses :1234/v1) +LLM_MODEL=Qwen/Qwen2.5-7B-Instruct # the model your server serves +LLM_API_KEY=local # local servers accept any value ``` **4. Fine-tune** @@ -140,7 +138,7 @@ llamafactory-cli train configs/train_lora.yaml ### Optional: LLM quality auditing -Each extracted conversation can be scored for **coherence, quality, and pairing**, dropping or splitting weak samples before training. It uses the OpenAI-compatible API, so it works with OpenAI **or any local server** (Ollama, vLLM, LM Studio). It's enabled automatically when `LLM_API_KEY` or `LLM_API_BASE_URL` is set (configure it in `.env`, step 3 above). +Each extracted conversation can be scored for **coherence, quality, and pairing**, dropping or splitting weak samples before training. It talks to a **local** OpenAI-compatible server (vLLM, LM Studio, llama.cpp) so your chat data stays on your machine. It's enabled automatically when `LLM_API_KEY` or `LLM_API_BASE_URL` is set (configure it in `.env`, step 3 above). To turn it off, set `LLM_VALIDATE=false` in `.env` (persistent) or pass `--skip-validation` for a single run. To disable **all** auditing at once — both this and the regex scan — use `--no-audit`. @@ -186,7 +184,7 @@ Singapore ships as the worked reference ([`sg.py`](ingest/redaction/sg.py): nati Regex can't catch everything (names, context-dependent secrets). With `--llm-redact`, an LLM additionally flags such spans into the **same report and the same `--redact` step** — it points at verbatim spans, never rewriting your text. To protect your data it **prefers a local endpoint**: set `LLM_API_BASE_URL` to a local OpenAI-compatible server; without one it refuses to use a hosted API unless you pass `--allow-cloud-redaction`. ```bash -LLM_API_BASE_URL=http://localhost:11434/v1 LLM_MODEL=qwen2.5 \ +LLM_API_BASE_URL=http://localhost:8000/v1 LLM_MODEL=Qwen/Qwen2.5-7B-Instruct \ python -m ingest --source telegram --llm-redact --redact replace ``` @@ -300,7 +298,7 @@ The pre-refactor, Windows-only workflow (which cloned LLaMA-Factory at HEAD) is Doppelganger is as much a **learning sandbox** as a tool: the aim is to explore the *full* AI toolbox for capturing how a person communicates, and to find what actually moves the needle on *"does this sound like me?"*. Today that's LoRA fine-tuning — everything below is exploratory (see the issue tracker for the live backlog). -**Shaping the model** — pre-training · fine-tuning (today) · alignment/DPO · continual learning · synthetic data / self-instruct · multi-LoRA personas & merging · distillation to on-device · PEFT comparison +**Shaping the model** — pre-training · fine-tuning · alignment/DPO · continual learning · synthetic data / self-instruct · multi-LoRA personas & merging · distillation to on-device · PEFT comparison **Giving it context & memory** — RAG · long-term memory + reflection · relationship/knowledge graph · style embeddings / user-conditioning · persona-prompt quiz · MCP diff --git a/example.env b/example.env index 22f142a..3caa359 100644 --- a/example.env +++ b/example.env @@ -2,27 +2,18 @@ # Every value here is OPTIONAL; with none set, ingestion still runs (the LLM # features just stay off). -# ── Optional LLM features ───────────────────────────────────────────────────── -# Used by the conversation quality auditor and the optional LLM redaction pass. -# Both speak the OpenAI-compatible API, so they work with OpenAI or any local -# server (Ollama, vLLM, LM Studio, llama.cpp). Running a LOCAL model keeps your -# chat data on your machine — the recommended setup for private data. - -# Enable/disable the quality auditor. Default: enabled when LLM_API_KEY or -# LLM_API_BASE_URL is set. Set to false to skip it entirely (no API calls). -LLM_VALIDATE=true - -# Model id. For a local server use whatever it serves (e.g. qwen2.5, llama3.1). -LLM_MODEL=gpt-4o-mini - -# API key. Required for hosted APIs; local servers usually accept any value. -LLM_API_KEY=your_api_key_here - -# OpenAI-compatible endpoint. Set this to use a local model, e.g. -# http://localhost:11434/v1 (Ollama) -# http://localhost:8000/v1 (vLLM) -# Leave unset to use OpenAI's hosted API. -# LLM_API_BASE_URL=http://localhost:11434/v1 +# ── Optional LLM features (quality auditor + LLM redaction) ─────────────────── +# The CORE pipeline (parse -> dataset + regex sensitive-data scan) needs NONE of +# this and runs with no setup. Uncomment below to ALSO enable the LLM auditor / +# redaction. +# +# Run a LOCAL OpenAI-compatible server so your chat data never leaves your machine +# (vLLM, LM Studio, llama.cpp). Serve an open model, then uncomment: +# +# LLM_VALIDATE=true +# LLM_API_BASE_URL=http://localhost:8000/v1 # vLLM (LM Studio uses :1234/v1) +# LLM_MODEL=Qwen/Qwen2.5-7B-Instruct # the model your server serves +# LLM_API_KEY=local # local servers accept any value # ── Optional: Hugging Face ──────────────────────────────────────────────────── # Only needed to download GATED models during training (e.g. Gemma). The default diff --git a/ingest/llm.py b/ingest/llm.py index b862acf..b0af03b 100644 --- a/ingest/llm.py +++ b/ingest/llm.py @@ -1,22 +1,21 @@ """Shared OpenAI-compatible LLM client. One client for every optional LLM feature (quality validation, LLM redaction). -It speaks the OpenAI Chat Completions API, so it works against OpenAI itself -*and* any local/self-hosted server that exposes that API — Ollama, vLLM, LM -Studio, llama.cpp's server, LiteLLM, etc. Running a local endpoint is the -privacy-preserving way to use these features, since your chat text never leaves -your machine. +It speaks the OpenAI Chat Completions API, which is the de-facto standard that +local/self-hosted servers also expose — vLLM, LM Studio, llama.cpp's server, +Ollama, LiteLLM, etc. For privacy, run a LOCAL endpoint so your chat text never +leaves your machine; that is the intended setup for this project. Environment variables: LLM_VALIDATE true/false. Default: enabled when LLM_API_KEY or LLM_API_BASE_URL is set, disabled otherwise. - LLM_API_BASE_URL OpenAI-compatible base URL. Set this for a local model, e.g. - http://localhost:11434/v1 (Ollama) or http://localhost:8000/v1 - (vLLM). Unset → OpenAI's hosted API. - LLM_MODEL Model id (default: gpt-4o-mini). For a local server use whatever - it serves, e.g. "qwen2.5" or "llama3.1". - LLM_API_KEY API key. Local servers usually accept any value; falls back to - OPENAI_API_KEY if unset. + LLM_API_BASE_URL Base URL of your local OpenAI-compatible server, e.g. + http://localhost:8000/v1 (vLLM) or http://localhost:1234/v1 + (LM Studio). + LLM_MODEL Model id your server serves — required to use the LLM features + (no default). Use the HF repo id, as vLLM / LM Studio do + (e.g. "Qwen/Qwen2.5-7B-Instruct"). + LLM_API_KEY API key. Local servers usually accept any value. """ import os @@ -25,7 +24,6 @@ MODEL_ENV = "LLM_MODEL" BASE_URL_ENV = "LLM_API_BASE_URL" API_KEY_ENV = "LLM_API_KEY" -DEFAULT_MODEL = "gpt-4o-mini" def base_url() -> str: @@ -33,7 +31,8 @@ def base_url() -> str: def model() -> str: - return os.environ.get(MODEL_ENV, "").strip() or DEFAULT_MODEL + """The configured model id, or empty string if unset (no default).""" + return os.environ.get(MODEL_ENV, "").strip() def is_local() -> bool: @@ -67,6 +66,11 @@ def get_client(): "The 'openai' package is required for LLM features. " "Install it with: pip install openai" ) + if not model(): + raise EnvironmentError( + f"{MODEL_ENV} is not set. Set it to the model your local server serves " + f"(e.g. Qwen/Qwen2.5-7B-Instruct), or set {VALIDATE_ENV}=false." + ) url = base_url() key = _api_key() if not key: From bf75479ec760b811f35019f02621ffde8a78333f Mon Sep 17 00:00:00 2001 From: NotYuSheng Date: Tue, 23 Jun 2026 23:31:55 +0800 Subject: [PATCH 10/15] docs: add local LLM (LM Studio) setup + model suggestions by hardware Co-Authored-By: Claude Opus 4.8 --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 33e1067..ac24575 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,32 @@ Each extracted conversation can be scored for **coherence, quality, and pairing* To turn it off, set `LLM_VALIDATE=false` in `.env` (persistent) or pass `--skip-validation` for a single run. To disable **all** auditing at once — both this and the regex scan — use `--no-audit`. +### Running a local LLM (recommended: LM Studio) + +The LLM features are designed to run against a **local** model so your chat data never leaves your machine. [LM Studio](https://lmstudio.ai) is the easiest way to get one running with a click-through UI: + +1. Install **LM Studio** and use its search to download a model (see the table below). +2. Open the **Developer** tab → **Start Server**. It serves an OpenAI-compatible API at `http://localhost:1234/v1`. +3. In `.env`, set: + ```dotenv + LLM_VALIDATE=true + LLM_API_BASE_URL=http://localhost:1234/v1 + LLM_MODEL= + LLM_API_KEY=local + ``` + +(Prefer the CLI? **vLLM** serves the same API at `http://localhost:8000/v1` with `--model Qwen/Qwen2.5-7B-Instruct`. **Ollama** also works at `http://localhost:11434/v1`.) + +**Which model?** The auditor/redactor just needs solid instruction-following and JSON output — a small model is plenty. Pick by your hardware (GGUF quants in LM Studio shrink the footprint): + +| Your hardware | Suggested model | Notes | +|---------------|-----------------|-------| +| CPU-only, or ≤8 GB VRAM / 16 GB RAM | **Qwen2.5-3B-Instruct** (Q4) | Fast and light; fine for scoring + PII spans | +| 8–16 GB VRAM | **Qwen2.5-7B-Instruct** (Q4/Q5) | Recommended balance of quality and speed | +| 24 GB+ VRAM | **Qwen2.5-14B-Instruct** | Best judgment on tricky/ambiguous cases | + +Tiny machine? **Qwen2.5-1.5B-Instruct** or **Llama-3.2-3B-Instruct** also work, with slightly noisier results. Any OpenAI-compatible model will do — these are just sensible starting points. + ## Privacy & Sensitive Data Fine-tuning on real chat history may unintentionally encode personal identifiers, confidential conversations, or sensitive content. From b395054f77a4e0311279d05fba94bc28646cf072 Mon Sep 17 00:00:00 2001 From: NotYuSheng Date: Tue, 23 Jun 2026 23:45:10 +0800 Subject: [PATCH 11/15] fix: SG postal detector no longer matches the leading digits of an NRIC A trailing negative lookahead stops sg_postal matching the first 6 digits of a longer token (e.g. NRIC S1234567D reading as S123456). Adds a regression test. Co-Authored-By: Claude Opus 4.8 --- ingest/redaction/sg.py | 6 ++++-- tests/test_redaction.py | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/ingest/redaction/sg.py b/ingest/redaction/sg.py index b78a391..6b33b72 100644 --- a/ingest/redaction/sg.py +++ b/ingest/redaction/sg.py @@ -82,11 +82,13 @@ def nric_valid(value: str) -> bool: ) # Postal code is 6 bare digits — far too noisy alone, so require an explicit -# "Singapore ", "S123456", or "S(123456)" context to keep precision up. +# "Singapore ", "S123456", or "S(123456)" context. The trailing lookahead +# stops it matching the first 6 digits of a longer token (e.g. the NRIC +# "S1234567D", which would otherwise read as "S123456"). make( "sg_postal", "POSTAL_CODE", "SG", - r"(?:[Ss]ingapore\s+|\bS\(?)\d{6}\)?", + r"(?:[Ss]ingapore\s+|\bS\(?)\d{6}\)?(?![\dA-Za-z])", severity="low", ) diff --git a/tests/test_redaction.py b/tests/test_redaction.py index 7630df8..f090276 100644 --- a/tests/test_redaction.py +++ b/tests/test_redaction.py @@ -66,6 +66,12 @@ def test_phone(self): self.assertIn("PHONE", _categories("call 9123 4567", ["SG"])) self.assertIn("PHONE", _categories("call +65 9123 4567", ["SG"])) + def test_postal_requires_context_and_not_nric(self): + self.assertIn("POSTAL_CODE", _categories("Singapore 560123", ["SG"])) + self.assertIn("POSTAL_CODE", _categories("address S123456", ["SG"])) + # Must NOT fire on the leading 6 digits of an NRIC. + self.assertNotIn("POSTAL_CODE", _categories("ic S1234567D", ["SG"])) + def test_locale_filtering(self): # SG detectors don't run when only universal locale is requested. self.assertNotIn("NRIC/FIN", _categories("ic S0000001I", [])) From 0eba0e8a7187279a41b798c3de83797d6033ffce Mon Sep 17 00:00:00 2001 From: NotYuSheng Date: Wed, 24 Jun 2026 00:33:05 +0800 Subject: [PATCH 12/15] feat: ASCII parrot+wordmark startup banner and demo GIF - ingest/banner.py: a parrot-in-a-mirror mascot (it mimics your voice; the mirror is the doppelganger) beside an ansi_shadow "Doppel/ganger" wordmark in truecolor amber, printed at CLI startup. DOPPELGANGER_NO_BANNER silences it. - README: embed demo/demo.gif (ingest + sensitive-data scan) at the top. - demo/: synthetic sample_export.json (gitignored exception) + the mascot source image and the build/convert scripts used to generate the art and GIF. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 4 ++ README.md | 4 ++ demo/build_final.py | 99 ++++++++++++++++++++++++++++++++++++++++ demo/demo.gif | Bin 0 -> 239193 bytes demo/img2ascii.py | 37 +++++++++++++++ demo/mascot.txt | 17 +++++++ demo/parrot-mirror.jpg | Bin 0 -> 11862 bytes demo/sample_export.json | 22 +++++++++ ingest/banner.py | 37 +++++++++++++++ ingest/cli.py | 3 ++ 10 files changed, 223 insertions(+) create mode 100644 demo/build_final.py create mode 100644 demo/demo.gif create mode 100644 demo/img2ascii.py create mode 100644 demo/mascot.txt create mode 100644 demo/parrot-mirror.jpg create mode 100644 demo/sample_export.json create mode 100644 ingest/banner.py diff --git a/.gitignore b/.gitignore index 2e45380..f93c2fe 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ ChatExport*/ # Tracked project config (re-include despite the broad *.json rule above) !configs/dataset_info.json +# Synthetic demo input — safe to commit and needed for the reproducible demo. +# (Generated demo outputs like demo/sample_sharegpt.json stay ignored via *.json.) +!demo/sample_export.json + # Personal training/export overrides — copy a tracked config to *.local.yaml and # edit that for your own model/hardware; it stays out of git. configs/*.local.yaml diff --git a/README.md b/README.md index ac24575..17bbc15 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,10 @@ License: MIT

+

+ Doppelganger CLI: ingest a chat export and scan it for sensitive data before training +

+ --- Doppelganger fine-tunes large language models (like Qwen) on your own chat conversations, capturing how *you* write. Built on top of [LLaMA-Factory](https://github.com/hiyouga/LLaMA-Factory), it turns a raw chat export into a [ShareGPT](https://github.com/hiyouga/LLaMA-Factory/blob/main/data/README.md)-formatted dataset for supervised fine-tuning (SFT), then trains a LoRA adapter on it. diff --git a/demo/build_final.py b/demo/build_final.py new file mode 100644 index 0000000..9fe74af --- /dev/null +++ b/demo/build_final.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Build the final banner (ingest/banner.py) and demo GIF (demo/demo.gif). + +Layout: parrot (left) + ansi_shadow "Doppel"/"ganger" (right), amber wordmark, +tagline centered beneath. Renders the GIF with agg's dracula theme. +""" +import json +import os +import subprocess + +import pyfiglet + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PY = os.path.join(ROOT, "venv", "bin", "python") +AGG = "/tmp/agg" +GAP = " " +TAG = "fine-tune an LLM to write like you" +CMD = "python -m ingest --source telegram --input demo/sample_export.json" +AMBER, RESET = "\x1b[1;38;2;242;176;76m", "\x1b[0m" + +parrot = open(os.path.join(ROOT, "demo/mascot.txt"), encoding="utf-8").read().rstrip("\n").split("\n") +PW = max(len(l) for l in parrot) + + +def _fig(t): + ls = [l.rstrip() for l in pyfiglet.figlet_format(t, font="ansi_shadow", width=200).rstrip("\n").split("\n")] + while ls and not ls[-1].strip(): ls.pop() + while ls and not ls[0].strip(): ls.pop(0) + return ls + + +word = _fig("Doppel") + _fig("ganger") +TOP = (len(parrot) - len(word)) // 2 +TOTAL_W = PW + len(GAP) + max(len(l) for l in word) + + +def rows(on, off): + r = [] + for i, pl in enumerate(parrot): + wl = word[i - TOP] if 0 <= i - TOP < len(word) else "" + wl = f"{on}{wl}{off}" if wl else "" + r.append((pl.ljust(PW) + GAP + wl).rstrip()) + r.append("") + r.append(TAG.center(TOTAL_W).rstrip()) # tagline centred under the whole logo + return r + + +def write_banner_module(): + body = "\n".join(rows("", "")) # sentinels; colourised at runtime + mod = ( + '"""ASCII startup banner: a parrot in a mirror (it mimics your voice; the\n' + 'mirror is the doppelganger) beside the wordmark. The wordmark is amber via\n' + 'truecolor ANSI. Regenerate via demo/build_final.py.\n' + 'Set DOPPELGANGER_NO_BANNER=1 to silence it."""\n\n' + 'import os\n\n' + '_AMBER = "\\x1b[1;38;2;242;176;76m" # truecolor amber\n' + '_RESET = "\\x1b[0m"\n\n' + '_BANNER = r"""\n' + body + '\n"""\n\n\n' + 'def print_banner() -> None:\n' + ' if os.environ.get("DOPPELGANGER_NO_BANNER"):\n' + ' return\n' + ' print(_BANNER.replace("", _AMBER).replace("", _RESET) + "\\n")\n' + ) + open(os.path.join(ROOT, "ingest/banner.py"), "w", encoding="utf-8").write(mod) + + +def render_gif(): + env = dict(os.environ, LLM_VALIDATE="false", DOPPELGANGER_NO_BANNER="1") + out = subprocess.run([PY] + CMD.split()[1:], cwd=ROOT, env=env, capture_output=True, text=True) + report = ((out.stdout or "") + (out.stderr or "")).split("\n") + + events, t = [], 0.0 + def emit(d, dt): + nonlocal t + t += dt + events.append([round(t, 3), "o", d]) + emit("\x1b[32m$\x1b[0m ", 0.3) + for ch in CMD: + emit(ch, 0.026) + emit("\r\n", 0.5) + for line in rows(AMBER, RESET) + report: + emit(line + "\r\n", 0.05) + emit("\x1b[32m$\x1b[0m ", 1.6) + + cast = os.path.join(ROOT, "demo/demo.cast") + with open(cast, "w", encoding="utf-8") as f: + f.write(json.dumps({"version": 2, "width": 94, "height": 34}) + "\n") + for ev in events: + f.write(json.dumps(ev, ensure_ascii=False) + "\n") + subprocess.run([AGG, "--font-size", "18", "--theme", "dracula", cast, + os.path.join(ROOT, "demo/demo.gif")], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +if __name__ == "__main__": + write_banner_module() + render_gif() + print("=== layout preview ===") + print("\n".join(rows("", ""))) diff --git a/demo/demo.gif b/demo/demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..799a2183181b1e1e39d51cd934fc54545aa42d6f GIT binary patch literal 239193 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∋I8oEKBO|AXE$${LX+<3 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 literal 0 HcmV?d00001 diff --git a/demo/img2ascii.py b/demo/img2ascii.py new file mode 100644 index 0000000..0c56bbe --- /dev/null +++ b/demo/img2ascii.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Convert an image to ASCII art via a brightness ramp. + +Usage: python demo/img2ascii.py PATH [cols] [--invert] +Transparent images are composited onto white first, so a dark subject on a +transparent background (e.g. an OpenMoji black glyph) renders as the dense end +of the ramp. +""" +import sys +from PIL import Image + +RAMP = " .:-=+*#%@" + + +def to_ascii(path: str, cols: int = 42, invert: bool = False) -> str: + img = Image.open(path) + if img.mode in ("RGBA", "LA") or (img.mode == "P" and "transparency" in img.info): + bg = Image.new("RGBA", img.size, (255, 255, 255, 255)) + img = Image.alpha_composite(bg, img.convert("RGBA")) + g = img.convert("L") + rows = max(1, int(cols * (g.height / g.width) * 0.50)) + g = g.resize((cols, rows)) + px = g.load() + ramp = RAMP[::-1] if invert else RAMP + lines = [] + for y in range(rows): + lines.append( + "".join( + ramp[int((255 - px[x, y]) / 255 * (len(ramp) - 1))] for x in range(cols) + ).rstrip() + ) + return "\n".join(lines) + + +if __name__ == "__main__": + args = [a for a in sys.argv[1:] if a != "--invert"] + print(to_ascii(args[0], int(args[1]) if len(args) > 1 else 42, "--invert" in sys.argv)) diff --git a/demo/mascot.txt b/demo/mascot.txt new file mode 100644 index 0000000..dc8fbf0 --- /dev/null +++ b/demo/mascot.txt @@ -0,0 +1,17 @@ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⡀ +⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⠶⠖⠛⠛⠋⠉⣿⣿⣿⣿⣷⣶⣤⣄⡀ +⠀⠀⠀⠀⠀⣠⡶⠛⠉⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⡀ +⠀⠀⠀⣠⡾⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆ +⠀⠀⣴⢋⣤⣴⣶⣦⣤⡀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⡿⠟⠋⠉⠉⠙⠻⢷⡀ +⠀⣸⣿⣿⣿⠿⠛⠻⣿⣿⣦⠀⠀⢀⣀⣤⣉⡙⠻⢿⣿⠏⠀⣠⠶⠚⠓⢶⣄⠀⠁ +⠀⣿⣿⣿⠁⠀⠸⠿⠊⣿⣿⠂⣴⠟⠁⠀⣿⣿⣷⣄⠙⠀⢸⠇⠀⠐⠿⠇⢹⡆ +⢸⣿⣿⣿⡀⠄⠀⠀⢀⣿⠇⣼⠃⠸⠁⠀⣿⣿⣄⣿⣇⠀⠸⣇⠠⠀⠀⠀⣼⠇ +⢸⡿⢿⣿⣿⣷⣶⣾⣿⣿⢰⡏⠀⠀⠀⠀⣿⣿⣿⣿⣿⡀⠀⠈⠛⠶⠶⠛⠁⠀⢠⡇ +⢸⡇⠈⠛⠿⣿⣿⣿⡿⠟⢸⣇⠀⠀⠀⠀⣿⣿⣿⣿⡿⠇⣀⡀⠀⠀⠀⢀⣠⣴⣿⡇ +⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⢰⡌⢳⣄⠀⠀⣿⣿⣿⠋⣠⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇ +⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠈⢷⡀⢹⡆⠀⣿⣿⠃⣼⠏⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇ +⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠳⣄⢻⡄⣿⡏⠐⢋⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇ +⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢷⡟⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇ +⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇ +⢸⣇⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇ +⠈⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠁ diff --git a/demo/parrot-mirror.jpg b/demo/parrot-mirror.jpg new file mode 100644 index 0000000000000000000000000000000000000000..17257be57a2fdfa77e688280333b33413946a825 GIT binary patch literal 11862 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██████╗ ██████╗ ██████╗ ██████╗ ███████╗██╗ +⠀⠀⠀⣠⡾⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆ ██╔══██╗██╔═══██╗██╔══██╗██╔══██╗██╔════╝██║ +⠀⠀⣴⢋⣤⣴⣶⣦⣤⡀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⡿⠟⠋⠉⠉⠙⠻⢷⡀ ██║ ██║██║ ██║██████╔╝██████╔╝█████╗ ██║ +⠀⣸⣿⣿⣿⠿⠛⠻⣿⣿⣦⠀⠀⢀⣀⣤⣉⡙⠻⢿⣿⠏⠀⣠⠶⠚⠓⢶⣄⠀⠁ ██║ ██║██║ ██║██╔═══╝ ██╔═══╝ ██╔══╝ ██║ +⠀⣿⣿⣿⠁⠀⠸⠿⠊⣿⣿⠂⣴⠟⠁⠀⣿⣿⣷⣄⠙⠀⢸⠇⠀⠐⠿⠇⢹⡆ ██████╔╝╚██████╔╝██║ ██║ ███████╗███████╗ +⢸⣿⣿⣿⡀⠄⠀⠀⢀⣿⠇⣼⠃⠸⠁⠀⣿⣿⣄⣿⣇⠀⠸⣇⠠⠀⠀⠀⣼⠇ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝ +⢸⡿⢿⣿⣿⣷⣶⣾⣿⣿⢰⡏⠀⠀⠀⠀⣿⣿⣿⣿⣿⡀⠀⠈⠛⠶⠶⠛⠁⠀⢠⡇ ██████╗ █████╗ ███╗ ██╗ ██████╗ ███████╗██████╗ +⢸⡇⠈⠛⠿⣿⣿⣿⡿⠟⢸⣇⠀⠀⠀⠀⣿⣿⣿⣿⡿⠇⣀⡀⠀⠀⠀⢀⣠⣴⣿⡇ ██╔════╝ ██╔══██╗████╗ ██║██╔════╝ ██╔════╝██╔══██╗ +⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⢰⡌⢳⣄⠀⠀⣿⣿⣿⠋⣠⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇ ██║ ███╗███████║██╔██╗ ██║██║ ███╗█████╗ ██████╔╝ +⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠈⢷⡀⢹⡆⠀⣿⣿⠃⣼⠏⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇ ██║ ██║██╔══██║██║╚██╗██║██║ ██║██╔══╝ ██╔══██╗ +⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠳⣄⢻⡄⣿⡏⠐⢋⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇ ╚██████╔╝██║ ██║██║ ╚████║╚██████╔╝███████╗██║ ██║ +⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢷⡟⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ +⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇ +⢸⣇⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇ +⠈⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠁ + + fine-tune an LLM to write like you +""" + + +def print_banner() -> None: + if os.environ.get("DOPPELGANGER_NO_BANNER"): + return + print(_BANNER.replace("", _AMBER).replace("", _RESET) + "\n") diff --git a/ingest/cli.py b/ingest/cli.py index 2cf2a5f..e3b6842 100644 --- a/ingest/cli.py +++ b/ingest/cli.py @@ -176,6 +176,9 @@ def main(argv=None) -> int: _load_dotenv() args = build_parser().parse_args(argv) + from ingest import banner + banner.print_banner() + try: adapter = get_adapter(args.source) except ValueError as e: From d0e93b091af7a9e1d31fed8a7cefb7fc0d5f0d34 Mon Sep 17 00:00:00 2001 From: NotYuSheng Date: Wed, 24 Jun 2026 00:35:22 +0800 Subject: [PATCH 13/15] fix: redact all occurrences of an LLM span; default missing Telegram sender - redactor.llm_scan_samples: locate EVERY non-overlapping occurrence of a flagged span (text.find loop), not just the first, so repeated names/numbers can't leak [security-high]. - telegram adapter: 'from' may be None (anonymous channel posts) -> default to 'Unknown' so sender_id stays a str and multi-speaker mode has no 'None:' prefix. - Tests for both. Co-Authored-By: Claude Opus 4.8 --- ingest/adapters/telegram.py | 5 ++++- ingest/redactor.py | 35 +++++++++++++++++++++-------------- tests/test_ingest.py | 15 +++++++++++++++ tests/test_redaction.py | 10 ++++++++++ 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/ingest/adapters/telegram.py b/ingest/adapters/telegram.py index 7d0bbae..323c2f0 100644 --- a/ingest/adapters/telegram.py +++ b/ingest/adapters/telegram.py @@ -64,7 +64,10 @@ def parse( for msg in chat.get("messages", []): if not _is_valid(msg): continue - sender = msg.get("from") + # "from" can be missing/None (e.g. anonymous channel posts); fall + # back to a label so sender_id stays a str (and multi-speaker + # mode doesn't emit a "None: " prefix). + sender = msg.get("from") or "Unknown" reply_to = msg.get("reply_to_message_id") msg_id = msg.get("id") messages.append( diff --git a/ingest/redactor.py b/ingest/redactor.py index 85edec5..fc9cea2 100644 --- a/ingest/redactor.py +++ b/ingest/redactor.py @@ -219,21 +219,28 @@ def llm_scan_samples(samples, client, model) -> List[dict]: if not (0 <= ti < len(turns)) or not span: continue text = turns[ti].get("text", "") - idx = text.find(span) - if idx < 0: + # Record every non-overlapping occurrence — a repeated name/number + # must not leak just because only the first was redacted. + start, located = 0, False + while True: + idx = text.find(span, start) + if idx < 0: + break + located = True + findings.append({ + "conversation": ci, + "turn": ti, + "role": turns[ti].get("role"), + "category": str(rf.get("category", "PII")), + "detector": "llm", + "severity": str(rf.get("severity", "medium")), + "start": idx, + "end": idx + len(span), + "preview": redaction.mask(span), + }) + start = idx + len(span) + if not located: print(f"[redactor] LLM span not found verbatim (conv {ci}, turn {ti}): {span!r}") - continue - findings.append({ - "conversation": ci, - "turn": ti, - "role": turns[ti].get("role"), - "category": str(rf.get("category", "PII")), - "detector": "llm", - "severity": str(rf.get("severity", "medium")), - "start": idx, - "end": idx + len(span), - "preview": redaction.mask(span), - }) return findings diff --git a/tests/test_ingest.py b/tests/test_ingest.py index 5fedbab..5f2b99d 100644 --- a/tests/test_ingest.py +++ b/tests/test_ingest.py @@ -118,6 +118,21 @@ def test_self_name_override(self): alice = [m for m in msgs if m.sender_id == "Alice"][0] self.assertTrue(alice.sender_is_self) + def test_missing_from_becomes_unknown(self): + # "from" can be missing/None (anonymous channel posts); sender_id must + # stay a str rather than None. + with tempfile.TemporaryDirectory() as d: + path = os.path.join(d, "result.json") + with open(path, "w", encoding="utf-8") as f: + json.dump({"personal_information": {"first_name": "Yu", "last_name": "Sheng"}, + "chats": {"list": [{"id": 1, "messages": [ + _msg(None, 100, "anon post"), + _msg(SELF, 110, "reply")]}]}}, f) + msgs = TelegramAdapter().parse(path) + anon = [m for m in msgs if m.timestamp == 100][0] + self.assertEqual(anon.sender_id, "Unknown") + self.assertFalse(anon.sender_is_self) + def test_undetectable_self_name_raises(self): # Without personal_information, auto-detection yields "" — which would # silently drop every conversation. Must fail loudly instead. diff --git a/tests/test_redaction.py b/tests/test_redaction.py index f090276..1a2fb9d 100644 --- a/tests/test_redaction.py +++ b/tests/test_redaction.py @@ -141,6 +141,16 @@ def test_verbatim_span_is_located_and_masked(self): self.assertEqual(finds[0]["end"], 12) self.assertNotIn("Alice", finds[0]["preview"]) # masked + def test_repeated_span_all_located(self): + # Every occurrence of a repeated span must be recorded, not just the first. + samples = [[{"role": "user", "text": "Alice told Alice about Alice"}, + {"role": "assistant", "text": "ok"}]] + client = _FakeClient( + '{"findings":[{"turn":0,"text":"Alice","category":"NAME","severity":"high"}]}' + ) + finds = redactor.llm_scan_samples(samples, client, "model") + self.assertEqual([f["start"] for f in finds], [0, 11, 23]) + def test_unlocatable_span_is_dropped(self): # Model paraphrased instead of copying -> can't verify -> skipped. client = _FakeClient( From 874e305711f456985ca2c6ce679da9f4bc7e600d Mon Sep 17 00:00:00 2001 From: NotYuSheng Date: Wed, 24 Jun 2026 06:54:53 +0800 Subject: [PATCH 14/15] fix: merge overlapping redaction spans; guard unmatched optional id group - redactor._replace_spans: merge overlapping spans and redact the full region (label = longest contributor), so a shorter span can't shadow part of a longer sensitive one [security-high]. - redaction.scan_text: skip a hit whose optional id group didn't match (m.span(id) == (-1,-1)) instead of crashing in mask(). - demo/build_final.py: read mascot.txt via a context manager. - Tests for merge behaviour and the optional-id guard. Co-Authored-By: Claude Opus 4.8 --- demo/build_final.py | 3 ++- ingest/redaction/__init__.py | 4 +++- ingest/redactor.py | 25 ++++++++++++++----------- tests/test_redaction.py | 30 +++++++++++++++++++++++++----- 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/demo/build_final.py b/demo/build_final.py index 9fe74af..67aa940 100644 --- a/demo/build_final.py +++ b/demo/build_final.py @@ -18,7 +18,8 @@ CMD = "python -m ingest --source telegram --input demo/sample_export.json" AMBER, RESET = "\x1b[1;38;2;242;176;76m", "\x1b[0m" -parrot = open(os.path.join(ROOT, "demo/mascot.txt"), encoding="utf-8").read().rstrip("\n").split("\n") +with open(os.path.join(ROOT, "demo/mascot.txt"), encoding="utf-8") as _f: + parrot = _f.read().rstrip("\n").split("\n") PW = max(len(l) for l in parrot) diff --git a/ingest/redaction/__init__.py b/ingest/redaction/__init__.py index a1f93d7..90de76a 100644 --- a/ingest/redaction/__init__.py +++ b/ingest/redaction/__init__.py @@ -130,10 +130,12 @@ def scan_text(text: str, locales: Optional[Iterable[str]] = None) -> "List[Findi # but report just the number). Otherwise the whole match is the value. report_id = "id" in det.pattern.groupindex for m in det.pattern.finditer(text): + start, end = m.span("id") if report_id else m.span() + if start == -1: # an optional ``id`` group that didn't match this hit + continue value = m.group("id") if report_id else m.group() if det.validator and not det.validator(value): continue - start, end = m.span("id") if report_id else m.span() findings.append( Finding( detector=det.name, diff --git a/ingest/redactor.py b/ingest/redactor.py index fc9cea2..f8feefa 100644 --- a/ingest/redactor.py +++ b/ingest/redactor.py @@ -93,18 +93,21 @@ def print_summary(report: dict, report_path: str, mode: str = "off") -> None: def _replace_spans(text: str, spans) -> str: """Replace ``(start, end, category)`` spans with ``[CATEGORY]`` placeholders. - On overlap, keep the longer/outermost span — so an inner ``DOMAIN`` can't - survive while its enclosing ``EMAIL`` is dropped, which would leave the email - username exposed. Sort by start ascending then end descending, greedily keep - non-overlapping spans, and apply right-to-left so earlier offsets stay valid. + Overlapping spans are **merged** so the full extent of every detected span is + redacted — no partial leaks (e.g. a shorter span can't shadow part of a longer + one). Each merged region is labelled by its longest contributing span. + Applied right-to-left so earlier offsets stay valid. """ - chosen = [] - last_end = 0 - for start, end, cat in sorted(set(spans), key=lambda s: (s[0], -s[1])): - if start >= last_end: - chosen.append((start, end, cat)) - last_end = end - for start, end, cat in reversed(chosen): + merged = [] # [start, end, label_cat, label_len] + for start, end, cat in sorted(set(spans)): + if merged and start < merged[-1][1]: # overlaps the previous region + region = merged[-1] + region[1] = max(region[1], end) + if end - start > region[3]: # longer span -> its label wins + region[2], region[3] = cat, end - start + else: + merged.append([start, end, cat, end - start]) + for start, end, cat, _ in reversed(merged): text = text[:start] + f"[{cat}]" + text[end:] return text diff --git a/tests/test_redaction.py b/tests/test_redaction.py index 1a2fb9d..9b8ff15 100644 --- a/tests/test_redaction.py +++ b/tests/test_redaction.py @@ -77,6 +77,19 @@ def test_locale_filtering(self): self.assertNotIn("NRIC/FIN", _categories("ic S0000001I", [])) +class OptionalIdGroupTest(unittest.TestCase): + def test_unmatched_optional_id_group_is_skipped(self): + # A detector whose ``id`` group is optional must not crash / mis-offset + # when that group doesn't match a given hit. + det = redaction.make("tmp_opt", "TMP", redaction.UNIVERSAL, r"X(?P\d+)?") + try: + self.assertNotIn("TMP", _categories("X here", [])) # id unmatched -> skipped + finds = [f for f in redaction.scan_text("X42", []) if f.category == "TMP"] + self.assertEqual(finds[0].value, "42") + finally: + redaction._REGISTRY.remove(det) + + class RegistryTest(unittest.TestCase): def test_no_duplicate_names(self): names = [d.name for d in redaction.iter_detectors()] @@ -173,16 +186,23 @@ def test_apply_replace_uses_llm_offsets(self): out = redactor.apply(self._samples(), "replace", llm_findings=llm) self.assertEqual(out[0][0]["text"], "hi I'm [NAME] from Acme") - def test_replace_spans_prefers_outer_span(self): + def test_replace_spans_merges_overlaps(self): from ingest.redactor import _replace_spans - # Partial overlap -> keep the earlier/outer span, one clean replacement. - self.assertEqual(_replace_spans("abcdef", [(0, 3, "X"), (2, 5, "Y")]), "[X]def") - # Nested: the inner span must not survive while its enclosing span is - # dropped (which would leave the uncovered prefix exposed). + # Overlapping spans merge so the WHOLE region is redacted (no partial + # leak); the merged region is labelled by its longest contributing span. + self.assertEqual(_replace_spans("abcdef", [(0, 3, "X"), (2, 5, "Y")]), "[X]f") self.assertEqual( _replace_spans("a@b.com x", [(0, 7, "EMAIL"), (2, 7, "DOMAIN")]), "[EMAIL] x", ) + # A longer, later-starting span must not be shadowed by a shorter earlier + # one — the whole region is covered and labelled EMAIL. + self.assertEqual( + _replace_spans("0123456789ABCDEFGHIJ", [(0, 8, "CTX"), (5, 20, "EMAIL")]), + "[EMAIL]", + ) + # Non-overlapping spans stay separate. + self.assertEqual(_replace_spans("a b c", [(0, 1, "A"), (4, 5, "C")]), "[A] b [C]") if __name__ == "__main__": From 301ce6c39cef719619ea16f3df0cebae163b505d Mon Sep 17 00:00:00 2001 From: NotYuSheng Date: Wed, 24 Jun 2026 07:09:59 +0800 Subject: [PATCH 15/15] fix: honour --redact when scan skipped; prioritize split repair; harden demo scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cli: decouple redaction application from the scan/report — --redact is now applied even with --skip-redact-scan/--no-audit, so chat data isn't silently left unredacted [security]. - validator: evaluate the 'split' action before the low-score drop, so over-merged samples are repaired instead of discarded. - demo scripts: configurable AGG path ($AGG), check=True on subprocess, usage guard in img2ascii, and a demo/README documenting the dev-only deps (pillow, pyfiglet, agg). - Tests for the two behaviours. Co-Authored-By: Claude Opus 4.8 --- demo/README.md | 31 ++++++++++++++++++++++++++++ demo/build_final.py | 7 ++++--- demo/img2ascii.py | 3 +++ ingest/cli.py | 28 +++++++++++++++---------- ingest/validator.py | 9 +++++--- tests/test_ingest.py | 49 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 demo/README.md diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..530dede --- /dev/null +++ b/demo/README.md @@ -0,0 +1,31 @@ +# demo/ + +Assets and the (dev-only) scripts used to generate the README banner/GIF. None of +this is needed to run Doppelganger — it's tooling for regenerating the visuals. + +| File | What it is | +|------|------------| +| `parrot-mirror.jpg` | Source image for the mascot | +| `mascot.txt` | The parrot converted to braille ASCII (committed art) | +| `sample_export.json` | **Synthetic** Telegram export used by the demo (safe, fake PII) | +| `demo.gif` | The README demo (ingest + sensitive-data scan) | +| `img2ascii.py` | Convert an image to ASCII (brightness ramp) | +| `build_final.py` | Rebuild `ingest/banner.py` and `demo/demo.gif` | + +## Regenerating + +These scripts need extra dev dependencies that the app itself does **not** require: + +```bash +pip install pillow pyfiglet # img2ascii.py / build_final.py +# plus the asciinema 'agg' renderer (https://github.com/asciinema/agg): +# cargo install --git https://github.com/asciinema/agg +# or download a release binary and set: export AGG=/path/to/agg +``` + +Then: + +```bash +python demo/img2ascii.py parrot-mirror.jpg 72 # preview the mascot conversion +python demo/build_final.py # rewrite the banner + demo.gif +``` diff --git a/demo/build_final.py b/demo/build_final.py index 67aa940..33014df 100644 --- a/demo/build_final.py +++ b/demo/build_final.py @@ -12,7 +12,7 @@ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PY = os.path.join(ROOT, "venv", "bin", "python") -AGG = "/tmp/agg" +AGG = os.environ.get("AGG", "agg") # asciinema agg on PATH; override with $AGG GAP = " " TAG = "fine-tune an LLM to write like you" CMD = "python -m ingest --source telegram --input demo/sample_export.json" @@ -67,7 +67,8 @@ def write_banner_module(): def render_gif(): env = dict(os.environ, LLM_VALIDATE="false", DOPPELGANGER_NO_BANNER="1") - out = subprocess.run([PY] + CMD.split()[1:], cwd=ROOT, env=env, capture_output=True, text=True) + out = subprocess.run([PY, *CMD.split()[1:]], cwd=ROOT, env=env, + capture_output=True, text=True, check=True) report = ((out.stdout or "") + (out.stderr or "")).split("\n") events, t = [], 0.0 @@ -90,7 +91,7 @@ def emit(d, dt): f.write(json.dumps(ev, ensure_ascii=False) + "\n") subprocess.run([AGG, "--font-size", "18", "--theme", "dracula", cast, os.path.join(ROOT, "demo/demo.gif")], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True, check=True) if __name__ == "__main__": diff --git a/demo/img2ascii.py b/demo/img2ascii.py index 0c56bbe..d51a6b6 100644 --- a/demo/img2ascii.py +++ b/demo/img2ascii.py @@ -34,4 +34,7 @@ def to_ascii(path: str, cols: int = 42, invert: bool = False) -> str: if __name__ == "__main__": args = [a for a in sys.argv[1:] if a != "--invert"] + if not args: + print("Usage: python demo/img2ascii.py PATH [cols] [--invert]", file=sys.stderr) + raise SystemExit(2) print(to_ascii(args[0], int(args[1]) if len(args) > 1 else 42, "--invert" in sys.argv)) diff --git a/ingest/cli.py b/ingest/cli.py index e3b6842..0ead228 100644 --- a/ingest/cli.py +++ b/ingest/cli.py @@ -207,27 +207,33 @@ def main(argv=None) -> int: if args.no_audit: print("[audit] All auditing disabled (--no-audit) — building dataset as-is.") + locales = [s.strip() for s in args.redact_locales.split(",") if s.strip()] + llm_findings = [] if not skip_scan: - locales = [s.strip() for s in args.redact_locales.split(",") if s.strip()] report = redactor.scan_samples(samples, locales=locales) - - llm_findings = [] if args.llm_redact: llm_findings = _run_llm_redaction(samples, args.allow_cloud_redaction) redactor.merge_llm_findings(report, llm_findings) - report_path = os.path.join(os.path.dirname(output) or ".", "redaction_report.json") redactor.write_report(report, report_path) redactor.print_summary(report, report_path, mode=args.redact) - if args.redact != "off": - before = len(samples) - samples = redactor.apply( - samples, args.redact, locales=locales, llm_findings=llm_findings - ) + + # --redact is an explicit request, so honour it even when the scan/report was + # skipped — otherwise the dataset would silently keep sensitive data. + if args.redact != "off": + if skip_scan: print( - f"[redactor] Applied --redact {args.redact}: " - f"{before} -> {len(samples)} samples." + f"[redactor] Scan skipped, but --redact {args.redact} was requested — " + "applying regex redaction (note: --llm-redact needs the scan)." ) + before = len(samples) + samples = redactor.apply( + samples, args.redact, locales=locales, llm_findings=llm_findings + ) + print( + f"[redactor] Applied --redact {args.redact}: " + f"{before} -> {len(samples)} samples." + ) if not skip_validation: samples = validate_samples(samples) diff --git a/ingest/validator.py b/ingest/validator.py index 103ebed..c7cd0ea 100644 --- a/ingest/validator.py +++ b/ingest/validator.py @@ -157,15 +157,18 @@ def validate_samples(samples): r["quality"] < QUALITY_THRESHOLD or r["pairing"] < PAIRING_THRESHOLD ) - if r["action"] == "drop" or low: - filtered.append((i, "dropped", r)) - elif r["action"] == "split": + # Try repair-by-split first: an over-merged sample scores low on + # coherence/pairing, so checking `low` before `split` would always drop + # the very samples split is meant to rescue. + if r["action"] == "split": pieces = [p for p in _apply_split(turns, r["split_after"]) if _has_both_roles(p)] if pieces: passed.extend(pieces) split_count += 1 else: filtered.append((i, "split-empty", r)) + elif r["action"] == "drop" or low: + filtered.append((i, "dropped", r)) else: passed.append(turns) diff --git a/tests/test_ingest.py b/tests/test_ingest.py index 5f2b99d..ca4ab30 100644 --- a/tests/test_ingest.py +++ b/tests/test_ingest.py @@ -256,6 +256,36 @@ def test_has_both_roles(self): self.assertFalse(_has_both_roles([{"role": "user"}, {"role": "user"}])) +class _FakeOpenAI: + """Stub OpenAI-compatible client returning canned JSON (no network).""" + def __init__(self, text): + import types + msg = types.SimpleNamespace(content=text) + resp = types.SimpleNamespace(choices=[types.SimpleNamespace(message=msg)]) + self.chat = types.SimpleNamespace( + completions=types.SimpleNamespace(create=lambda **kw: resp)) + + +class ValidatorSplitPriorityTest(unittest.TestCase): + def test_split_runs_even_when_scores_are_low(self): + from ingest import validator, llm + canned = ('{"coherence":0.2,"quality":0.2,"pairing":0.2,' + '"action":"split","split_after":[1],"reason":"two convos"}') + orig_get, orig_should = llm.get_client, llm.should_validate + llm.get_client = lambda: _FakeOpenAI(canned) + llm.should_validate = lambda: True + os.environ["LLM_MODEL"] = "x" + try: + sample = [{"role": "user", "text": "a"}, {"role": "assistant", "text": "b"}, + {"role": "user", "text": "c"}, {"role": "assistant", "text": "d"}] + out = validator.validate_samples([sample]) + # Low scores would previously drop it; now split runs first -> 2 pieces. + self.assertEqual(len(out), 2) + finally: + llm.get_client, llm.should_validate = orig_get, orig_should + os.environ.pop("LLM_MODEL", None) + + class RegistryTest(unittest.TestCase): def test_telegram_registered(self): self.assertIn("telegram", available_sources()) @@ -282,6 +312,25 @@ def test_unknown_source_exit_code(self): from ingest.cli import main self.assertEqual(main(["--source", "nope"]), 2) + def test_redact_applies_even_when_scan_skipped(self): + # --redact must still redact when the scan is skipped (no silent leak). + from ingest.cli import main + os.environ["LLM_VALIDATE"] = "false" + with tempfile.TemporaryDirectory() as d: + inp = os.path.join(d, "result.json") + with open(inp, "w", encoding="utf-8") as f: + json.dump({"personal_information": {"first_name": "Yu", "last_name": "Sheng"}, + "chats": {"list": [{"id": 1, "messages": [ + _msg("Alice", 100, "mail me at a@b.com"), + _msg(SELF, 110, "ok")]}]}}, f) + out = os.path.join(d, "out.json") + rc = main(["--source", "telegram", "--input", inp, "--output", out, + "--skip-redact-scan", "--redact", "replace"]) + self.assertEqual(rc, 0) + blob = json.dumps(json.load(open(out, encoding="utf-8"))) + self.assertIn("[EMAIL]", blob) + self.assertNotIn("a@b.com", blob) + if __name__ == "__main__": unittest.main()