LLM, ์์ด์ ํธ, ์ธ๋ถ API๋ก ํ ์คํธ๋ฅผ ๋ณด๋ด๊ธฐ ์ ์ ๋ฏผ๊ฐํ ๊ฐ์ ๊ฐ๋ฆฌ๊ณ , ํ์ํ ๊ฒฝ์ฐ ๊ฐ์ ์ธ์ ์์ ๋ค์ ๋๋๋ฆฌ๋ ๋ง์คํน API/SDK์ ๋๋ค.
ContextCloak๋ LLM์ด ์๋๋๋ค. ์ธ๋ถ ์ฒ๋ฆฌ ๋จ๊ณ ์์ ๋์ด๋ ๋ณดํธ ๋ ์ด์ด์ ๋๋ค.
์๋ณธ ๋ฌธ์ฅ
-> ContextCloak๊ฐ ๋ฏผ๊ฐ์ ๋ณด ํ์ง
-> ์ธ๋ถ๋ก ๋ณด๋ผ ๋ฌธ์ฅ ์์ฑ
-> ์ธ๋ถ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ์ ๋จ์ ์์ ๊ฐ
-> ๊ฐ์ ์ธ์
์์ ์๋ฌธ ๋งฅ๋ฝ์ผ๋ก ๋๋๋ฆฌ๊ธฐ
์๋ฅผ ๋ค์ด API ํค, ์ด๋ฉ์ผ, ์ ํ๋ฒํธ, ๋ด๋ถ IP ๊ฐ์ ๊ฐ์ ์ธ๋ถ๋ก ๋ณด๋ผ ๋ฌธ์ฅ์์ [REDACTED_*] ํํ์ ์์ ๊ฐ์ผ๋ก ๋ฐ๋๋๋ค. ์๋ณธ๊ฐ๊ณผ ์์ ๊ฐ์ ์ฐ๊ฒฐ ์ ๋ณด๋ ๋ณต์์ ์ํด ์ธ์
์์ ๊ด๋ฆฌํ๋ฉฐ, ๋ก๊ทธ์๋ ์๋ณธ๊ฐ์ ๋จ๊ธฐ์ง ์๋๋ก ์ค๊ณ๋์ด ์์ต๋๋ค.
์๋ ๋ช ๋ น์ด๋ Windows PowerShell ๊ธฐ์ค์ ๋๋ค. macOS/Linux๋ uv ๊ณต์ ์ค์น ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํด ๋์ผํ
uv/git๋ช ๋ น์ ์ ธ์ ๋ง๊ฒ ์คํํ์ธ์.
- Python 3.11 ์ด์
- uv (Windows PowerShell:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex") - git
git clone https://github.com/Kyuhan1230/contextCloak.git
cd contextCloakuv syncํ
์ด๋ธ(CSV/TSV/Parquet) ๋ง์คํน, ์น UI "ํ
์ด๋ธ" ํญ, CLI masksยทprotect ๋ช
๋ น๊น์ง ์ฐ๋ ค๋ฉด
pandas/pyarrow๊ฐ ํ์ํ table extra๋ฅผ ํจ๊ป ์ค์นํ์ธ์.
uv sync --extra table์๋ ๊ฐ์๋ ์๋ณธ ํ์ผ์ ์๋ ๋ณํ์ ํ์ง ์์ต๋๋ค. ํ์ํ ๋ ์ฌ์ฉ์๊ฐ ์ง์
protect๋ฅผ ์คํํ๋ฉด ์๋ณธ ์์ ๋ณ๋์ ๋ง์คํน ๋ณต์ฌ๋ณธ์ ๋ง๋ญ๋๋ค.
# data/๋ ๊ทธ๋๋ก ๋๊ณ data-masked/ ์์ฑ
uv run contextcloak protect .\data
# ์ถ๋ ฅ ์ด๋ฆ์ ์ง์ ์ง์
uv run contextcloak protect .\records.csv --output .\records-for-ai.csv
# ๊ฐ์ฅ ์์ ํ ๋ฐฉ์: ์๋ณธ์ AI workspace ๋ฐ์ ๋๊ณ ๋ณต์ฌ๋ณธ๋ง ์์ผ๋ก ์์ฑ
uv run contextcloak protect C:\private-data --output .\data-masked- ์ ๋ ฅ์ CSV, TSV, Parquet ํ์ผ ๋๋ ์ด ํ์ผ๋ค์ ๋ด์ ๋๋ ํฐ๋ฆฌ์ ๋๋ค.
- ์๋ณธ์ ์์ ยท์ด๋ํ์ง ์์ผ๋ฉฐ, ์ด๋ฏธ ์กด์ฌํ๋ ์ถ๋ ฅ์ ๋ฎ์ด์ฐ์ง ์์ต๋๋ค.
- ๋๋ ํฐ๋ฆฌ์์๋ ์ง์ ํ ์ด๋ธ๋ง ์ฒ๋ฆฌํ๊ณ ๋ค๋ฅธ ํ์ผ์ ๋ณต์ฌํ์ง ์์ต๋๋ค.
- ์ถ๋ ฅ์๋ ContextCloak ๋ณต์์ฉ ๋งคํ์ ๋จ๊ธฐ์ง ์์ต๋๋ค. ์๋ณธ์ด ๊ธฐ์ค ๋ฐ์ดํฐ์ ๋๋ค.
- AI ๋๊ตฌ์๋ ์๋ณธ
data/๊ฐ ์๋๋ผ ์์ฑ๋data-masked/๋ง ์ ๊ณตํ์ธ์.
๊ฐ๋ฅํ๋ฉด ์๋ณธ ํด๋ ์์ฒด๋ฅผ AI workspace ๋ฐ์ ๋๋ ๊ฒ์ด ๊ฐ์ฅ ๊ฐํ ๊ฒฝ๊ณ์
๋๋ค.
protected_paths hook์ ์ค์ ๋ฐฉ์ง๋ฅผ ๋์ง๋ง ์ด์์ฒด์ ์ ํ์ผ ๊ถํ์ ๋์ ํ์ง ์์ต๋๋ค.
๋ณต์์ด ํ์ํ ์
๋ฌด๋ ๊ธฐ์กด Web UI, /v1/mask/table API ๋๋ SDK์ ์ธ์
๊ธฐ๋ฐ
ํ๋ฆ์ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
NER(๊ฐ์ฒด๋ช
์ธ์) ํ์ง๊น์ง ์ฐ๋ ค๋ฉด torch/transformers๊ฐ ํ์ํ ner extra๋ฅผ
์ค์นํ์ธ์. ๊ธฐ๋ณธ(uv sync)๋ง์ผ๋ก๋ ์ ๊ท์ ํ์ง๋ง ๋์ํฉ๋๋ค.
uv sync --extra neruv run contextcloak-server๋๋ ์ง์ uvicorn์ผ๋ก ์คํํ ์ ์์ต๋๋ค.
uv run python -m uvicorn contextcloak.server.run:app --host 127.0.0.1 --port 18080์ํ ํ์ธ:
curl http://127.0.0.1:18080/health์์ ์๋ต:
{"status":"ok"}๋ธ๋ผ์ฐ์ ์์ ์๋ ์ฃผ์๋ฅผ ์ฝ๋๋ค.
http://127.0.0.1:18080/ui
์น UI์์ ํ์ธํ ์ ์๋ ๊ฒ:
- ์๋ณธ ๋ฌธ์ฅ์์ ๋ฏผ๊ฐํ ๊ฐ์ด ์ด๋ป๊ฒ ๊ฐ๋ ค์ง๋์ง
- ์ธ๋ถ๋ก ๋ณด๋ผ ๋ฌธ์ฅ์ ์๋ณธ๊ฐ์ด ๋น ์ง๋์ง
- ๊ฐ์ ์ธ์ ์์ ๊ฐ๋ฆฐ ๊ฐ์ ๋ค์ ๋๋๋ฆด ์ ์๋์ง
- ํ์ฌ ํ์ง ๋ฐฉ์์ด
์ ๊ท์์ธ์ง์ ๊ท์ + NER์ธ์ง - ๋คํฌ/๋ผ์ดํธ/์์คํ ํ ๋ง ์ ํ
๋ฐฐ์ง์ ๋ง์ฐ์ค๋ฅผ ์ฌ๋ฆฌ๋ฉด ํ์ฌ ๋ก์ปฌ API์ ํ์ง ๋ชจ๋ ์ค๋ช ์ด ํ์๋ฉ๋๋ค.
์น UI๋ same-origin ContextCloak API๋ง ํธ์ถํฉ๋๋ค. ์ค์ ์ธ๋ถ LLM์ด๋ SaaS๋ก ์ ๋ ฅ์ ์ ์กํ์ง ์์ต๋๋ค.
Windows ์ฝ์์์ ํ๊ธ ์ถ๋ ฅ์ด ๊นจ์ง๋ค๋ฉด PowerShell์์ ์๋์ฒ๋ผ ํ๊ฒฝ๋ณ์๋ฅผ ์ง์ ํ ๋ค ์คํํ์ธ์.
$env:PYTHONIOENCODING="utf-8" uv run python script.py
from contextcloak import ContextCloak
agent = ContextCloak(base_url="http://127.0.0.1:18080")
session = agent.session(ttl_seconds=3600) # session_id๋ฅผ ์๋ตํ๋ฉด ์๋ฒ๊ฐ ์๋ ์์ฑ
masked = session.mask_text('api_key="sk-12345678901234567890"')
print(masked.masked_text)
external_response = "๋ถ์ ๊ฒฐ๊ณผ: [REDACTED_CREDENTIALS] ๊ฐ์ด ์ค์ ๋์ด ์์ต๋๋ค."
restored = session.unmask_text(external_response)
print(restored)
session.close()
agent.close()์์ ์ถ๋ ฅ:
api_key="[REDACTED_CREDENTIALS]"
๋ถ์ ๊ฒฐ๊ณผ: sk-12345678901234567890 ๊ฐ์ด ์ค์ ๋์ด ์์ต๋๋ค.
์ฃผ์ endpoint:
GET /health
POST /v1/sessions
POST /v1/mask/text
POST /v1/unmask/text
DELETE /v1/sessions/{session_id}
GET /ui
GET /ui/status
POST /ui/external-response
๋ง์คํน ์์ฒญ:
POST /v1/mask/text
Content-Type: application/json
{
"session_id": "analysis-001",
"text": "admin email is hong.gildong@corp.example and host is 10.23.17.44"
}์๋ต ์์:
{
"masked_text": "admin email is [REDACTED_PII_EMAIL] and host is [REDACTED_INFRA]",
"session_id": "analysis-001",
"detected_categories": ["PII_EMAIL", "INFRA"],
"placeholder_counts": {"PII_EMAIL": 1, "INFRA": 1},
"strategies_applied": {"PII_EMAIL": "reversible", "INFRA": "reversible"},
"restorable": true
}๋ณต์ ์์ฒญ:
POST /v1/unmask/text
Content-Type: application/json
{
"session_id": "analysis-001",
"text": "review [REDACTED_PII_EMAIL] and [REDACTED_INFRA]"
}ContextCloak๋ ์ ๊ท์ ๊ธฐ๋ฐ ํ์ง๋ฅผ ๊ธฐ๋ณธ์ผ๋ก ์ฌ์ฉํฉ๋๋ค. ์ค์ ๊ณผ ๋ชจ๋ธ ์ํ์ ๋ฐ๋ผ NER๊ฐ ํจ๊ป ํ์ฑํ๋ ์ ์์ต๋๋ค.
์น UI์ ํ์ง ๋ฐฐ์ง๋ ์๋์ฒ๋ผ ํ์๋ฉ๋๋ค.
ํ์ง: ์ ๊ท์
ํ์ง: ์ ๊ท์ + NER
GET /ui/status๋ ์น UI๊ฐ ํ์ํ ํ์ง ๋ชจ๋ ์ํ๋ฅผ ๋ฐํํฉ๋๋ค.
{
"detection_mode": "ํ์ง: ์ ๊ท์ + NER",
"ner_requested": true,
"ner_config_enabled": true,
"ner_model_exists": true,
"ner_active": true,
"detail": "์ ํด์ง ํจํด๊ณผ NER(๊ฐ์ฒด๋ช
์ธ์)๋ฅผ ์ฌ์ฉํด ๋ฏผ๊ฐ์ ๋ณด ํ๋ณด๋ฅผ ์ฐพ์ต๋๋ค."
}๋ฃฐ๊ณผ ์์ฒญ ์ต์ ์ ๋ฐ๋ผ ํ์ ๋ฐฉ์์ ์กฐ์ ํ ์ ์์ต๋๋ค.
| strategy | ์์ ์ ๋ ฅ | ์์ ์ถ๋ ฅ | ์ฉ๋ |
|---|---|---|---|
reversible |
api_key="sk-..." |
api_key="[REDACTED_CREDENTIALS]" |
๊ฐ์ ์ธ์ ์์ ๋ณต์ํด์ผ ํ๋ ๊ฐ |
partial |
10.23.17.44 |
10.x.xx.xxx |
์ผ๋ถ ํํ๋ง ๋ณด์ฌ๋ ๋๋ ๊ฐ |
pseudonym |
James Bond |
PERSON_001 |
์ฌ๋/์๋น์ค/์ ๋ฌด ๊ฐ์ฒด alias |
passthrough |
TI-101 |
TI-101 |
์ ์ฑ ์ ๊ทธ๋๋ก ํต๊ณผํ๋ ๊ฐ |
์์ฒญ ์์:
masked = session.mask_text(
"operator James Bond connected from 10.23.17.44",
options={
"category_strategies": {
"PERSON": "pseudonym",
"INFRA": "partial",
}
},
)
print(masked.masked_text)CSV/DataFrame/Parquet ํ ์ด๋ธ์ ์ปฌ๋ผ dtype๊ณผ ์ํ ๊ฐ์ ๊ธฐ์ค์ผ๋ก ์ ๋ต์ ์ถ๋ก ํ๊ณ , credential ํ์์ ์ผ๋ถ ํ์ ๋์น์ง ์๋๋ก ์ ์ฒด non-null ๊ฐ์ ๊ฒ์ฌํฉ๋๋ค.
| strategy | ์ ์ฉ ๋์ | ๋์ |
|---|---|---|
standard_scale |
์ซ์ ์ปฌ๋ผ | ํ๊ท ๊ณผ ํ์คํธ์ฐจ๋ก ๋ณํํ๊ณ ๊ฐ์ ์ธ์ ์์ ๋ณต์ |
pseudonym |
๋ฌธ์์ด ์ปฌ๋ผ | PERSON_001 ๊ฐ์ session-local alias๋ก ์นํ |
partial |
IP ๋ฑ ์ผ๋ถ ํ์์ ์ ์งํ ๊ฐ | ์ผ๋ถ ๊ตฌ์กฐ๋ง ๋จ๊ธฐ๊ณ ๊ฐ ์นํ |
date_shift |
datetime dtype, ISO date/datetime ๋ฌธ์์ด | -371์ผ ์ด๋ํด ์์ผ์ ์ ์งํ๊ณ ๊ฐ์ ์ธ์
์์ ๋ณต์ |
passthrough |
bool ๋๋ ๋ช ์์ ์ผ๋ก ํต๊ณผ์ํจ ์ปฌ๋ผ | ๊ฐ์ ๋ณ๊ฒฝํ์ง ์์ |
date_shift์ ์ด๋๊ฐ์ -371์ผ, ์ฆ 53 * 7์ผ์
๋๋ค. ๋ฐ๋ผ์ ๋ ์ง๋ ๋ฌ๋ผ์ง์ง๋ง ์์ผ ํน์ฑ์ ์ ์ง๋ฉ๋๋ค. CSV์ ISO YYYY-MM-DD ๋ ์ง ๋ฌธ์์ด๊ณผ Parquet์ datetime ์ปฌ๋ผ์ ์๋ ์ ์ฉ๋๋ฉฐ, ์ปฌ๋ผ override๋ก passthrough๋ฅผ ์ง์ ํ๋ฉด ์ด๋ํ์ง ์์ต๋๋ค.
| ํ๊ฒฝ ๋ณ์ | ๊ธฐ๋ณธ๊ฐ | ์ค๋ช |
|---|---|---|
CONTEXTCLOAK_HOST |
127.0.0.1 |
API ์๋ฒ bind host |
CONTEXTCLOAK_PORT |
18080 |
API ์๋ฒ port |
CONTEXTCLOAK_BASE_URL |
http://127.0.0.1:18080 |
SDK๊ฐ ํธ์ถํ API ์๋ฒ URL |
CONTEXTCLOAK_CONFIG_DIR |
๊ธฐ๋ณธ config ๊ฒฝ๋ก | ๋ฃฐ/ํ๋ก์ ํธ ์ค์ ๋๋ ํฐ๋ฆฌ |
CONTEXTCLOAK_PROJECTS_ROOT |
~/Projects |
projects.yaml์ ํ๋ก์ ํธ ์์ ๋๋ ํฐ๋ฆฌ |
CONTEXTCLOAK_PROJECT |
์์ | ์ฌ์ฉํ project ์ด๋ฆ |
CONTEXTCLOAK_USE_NER |
true |
NER ์ฌ์ฉ ์์ฒญ ์ฌ๋ถ |
CONTEXTCLOAK_VAULT_PATH |
logs/contextcloak-vault.sqlite |
SQLite session ๋ณต์ ๋งคํ ์ ์ฅ์ |
์์:
$env:CONTEXTCLOAK_USE_NER="false"
uv run contextcloak-serverSQLite Vault ์์:
$env:CONTEXTCLOAK_VAULT_PATH="C:\ContextCloak\vault.sqlite"
uv run contextcloak-serverSQLite Vault ํ์ผ์๋ ๋ณต์์ ํ์ํ ์๋ณธ ๋ฏผ๊ฐ๊ฐ์ด ์ ์ฅ๋ฉ๋๋ค. ์ด์ ํ๊ฒฝ์์๋ ํ์ผ ๊ถํ, ๋์คํฌ ์ํธํ, ๋ฐฑ์ ์ ์ฑ , ๋ณด๊ด ๊ธฐ๊ฐ์ ๋ณ๋๋ก ํต์ ํด์ผ ํฉ๋๋ค.
- ๋ง์คํน์ ์ ์ถ ์ํ์ ๋ฎ์ถ๋ ๋ณดํธ ๋ ์ด์ด์ด์ง, ์ ์ฒด ๋ณด์ ํต์ ๋ฅผ ๋์ฒดํ์ง ์์ต๋๋ค.
- ๋ณต์์ ๊ฐ์ ์ธ์ ์ ์ฐ๊ฒฐ ์ ๋ณด์ ์์กดํฉ๋๋ค. ์ธ์ ID์ TTL์ ์ ๋ฌด ๋จ์์ ๋ง๊ฒ ๋ถ๋ฆฌํ์ธ์.
- ์ธ๋ถ LLM/API์๋ ์๋ณธ๊ฐ์ด ์๋๋ผ ๋ง์คํน๋ ๋ฌธ์ฅ๋ง ์ ๋ฌํ๋ ๊ตฌ์กฐ๋ก ํธ์ถ ์ฝ๋๋ฅผ ์์ฑํด์ผ ํฉ๋๋ค.
- ์ด์ ํ๊ฒฝ์์๋ ์ธ์ฆ, ์ ๊ทผ ์ ์ด, ๊ฐ์ฌ ์ ์ฑ , ์์ Vault ์ ์ฅ์๋ฅผ ๋ณ๋๋ก ์ค๊ณํด์ผ ํฉ๋๋ค.
- ๋ฏผ๊ฐ ์๋ณธ๊ฐ์ ๋ก๊ทธ์ ๋จ๊ธฐ์ง ์๋ ์ ์ฑ ์ ์ ์งํด์ผ ํฉ๋๋ค.
| ์์ญ | ์ํ |
|---|---|
| ํ ์คํธ ๋ง์คํน/๋ณต์ API | ์ง์ |
| Python SDK | ์ง์ |
| ์ธ์ ๊ธฐ๋ฐ ๋ณต์ | ์ง์ |
| ์น UI | ์ง์ |
| ์ ๊ท์ ํ์ง | ์ง์ |
| NER ํ์ง | ์ค์ /๋ชจ๋ธ ์ํ์ ๋ฐ๋ผ ์ง์ (uv sync --extra ner ํ์) |
| DataFrame/CSV/TSV/Parquet ๋ก์ปฌ ๋ง์คํน ์๋น์ค | ์ ๊ณต (uv sync --extra table ํ์) |
| CSV/TSV/Parquet ์๋ ๋ณดํธ ๋ณต์ฌ๋ณธ CLI | ์ง์ (contextcloak protect) |
| ์๋ณธ ํด๋ ์๋ ๊ฐ์ยท์๋ ๋ณํ | ์ง์ํ์ง ์์ (๋ช ์์ ์คํ ์์น) |
| ์๊ฒฉ Parquet ์ ๋ก๋ API | ์์ |
| ์ธ์ฆ/๊ถํ | ์ด์ ๋ฐฐํฌ ์ ์ถ๊ฐ ํ์ |
๊ธฐ์กด secureagent import, CLI, ํ๊ฒฝ๋ณ์๋ ํธํ์ ์ํด ์ ์งํฉ๋๋ค. ๋ฐํ์ ๊ตฌํ ๊ธฐ์ค์ contextcloak.*์ด๋ฉฐ, ์ ์ฝ๋์์๋ contextcloak import์ CONTEXTCLOAK_* ํ๊ฒฝ๋ณ์๋ฅผ ์ฌ์ฉํ์ธ์.
์ ํ ๋ณธ์ ์ Python SDK๊ฐ HTTP API๋ฅผ ํธ์ถํ๊ณ , API๊ฐ ์๋น์คยทํ์งยทVault ๊ณ์ธต์ ์กฐํฉํ๋ ๊ตฌ์กฐ์ ๋๋ค.
Python SDK
-> HTTP API
-> masking/session services
-> detector, policy, table transformer
-> SQLite Vault
์ฌ์ฉ์ ๋ช
์ ์คํ
-> protect CLI
-> ์๋ณธ ์ ์ง + ๋ณ๋ ๋ง์คํน ๋ณต์ฌ๋ณธ
- SDK๋ Vault๋ฅผ ์ง์ ์์ฑํ๊ฑฐ๋ ์ฝ์ง ์์ต๋๋ค.
core๋ ํ์ง์ ๋ง์คํน ๊ท์น์,services๋ ์ ์ค์ผ์ด์ค ์กฐํฉ์ ๋ด๋นํฉ๋๋ค.api์server๋ HTTP ๊ณ์ฝ๊ณผ ์คํ ํ๊ฒฝ์ ๋ด๋นํฉ๋๋ค.proxy์mitm์ ํธํยท์ง๋จ ๊ฒฝ๋ก์ด๋ฉฐ ๊ธฐ๋ณธ SDK/API ํ๋ฆ์ด ์๋๋๋ค.- DataFrame/Parquet ์ฒ๋ฆฌ๋ ๋ก์ปฌ ์๋น์ค์ Web UI์์ ์ ๊ณตํ๋ฉฐ ํ์ฌ HTTP SDK ๊ณต๊ฐ API์๋ ํฌํจํ์ง ์์ต๋๋ค.
protect๋ ๋ณ๋ ๋ณต์ฌ๋ณธ๋ง ๋ง๋ค๊ณ ๋ณต์ metadata๋ฅผ ์ฆ์ ํ๊ธฐํ๋ ๋ก์ปฌ ์ผํ์ฑ ํ๋ฆ์ ๋๋ค.
๊ตฌํ๊ณผ ํจ๊ป ์ ์งํด์ผ ํ๋ ๊ณ์ฝ์ ๊ฐ ๋ชจ๋ README์ ๋ก๋๋ค.
| ์์ญ | ๋ฌธ์ |
|---|---|
| ํจํค์ง ๊ตฌ์กฐ์ ๊ณต๊ฐ API | contextcloak/README.md |
| ์ค์ ๊ณผ ํ์ง ๋ฃฐ | config/README.md |
| Web UI | contextcloak/webui/README.md |
| ํ ์ด๋ธ ๋ง์คํน | contextcloak/table/README.md |
| Claude Code hooks | contextcloak/hooks/README.md |
| Vault | contextcloak/vault/README.md |
| MITM ์ง๋จ ๊ฒฝ๋ก | contextcloak/mitm/README.md |
| ์ด์ยท๊ฒ์ฆ ์คํฌ๋ฆฝํธ | scripts/README.md |
ํ ์คํธ ์คํ:
uv run pytest์น UI ๊ด๋ จ ํ ์คํธ๋ง ์คํ:
uv run pytest tests/test_web_ui.pyํ์ง ํ์ง ๋ฆฌํฌํธ:
uv run python scripts\evaluate_detection_quality.pyPrivacy Filter ๋ชจ๋ธ ์ค์น์ ๋ก์ปฌ ๋ก๋ ๊ฒ์ฆ (uv sync --extra ner ๋จผ์ ํ์):
uv run python scripts\setup_privacy_filter_model.py
uv run python scripts\setup_privacy_filter_model.py --skip-download
uv run pytest -q tests/test_privacy_filter_local_model.py