kintsugi 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kintsugi/__init__.py +51 -0
- kintsugi/_data/__init__.py +1 -0
- kintsugi/_data/attacks.py +207 -0
- kintsugi/_data/framings.py +133 -0
- kintsugi/_data/frequency_ja.py +5832 -0
- kintsugi/_data/frequency_words.py +53 -0
- kintsugi/_data/locales.py +66 -0
- kintsugi/attacks.py +75 -0
- kintsugi/cli.py +151 -0
- kintsugi/detect.py +3736 -0
- kintsugi/engine.py +163 -0
- kintsugi/framings.py +135 -0
- kintsugi/frequency.py +153 -0
- kintsugi/judge.py +138 -0
- kintsugi/locales.py +89 -0
- kintsugi/providers.py +307 -0
- kintsugi/transforms.py +229 -0
- kintsugi-0.1.0.dist-info/METADATA +212 -0
- kintsugi-0.1.0.dist-info/RECORD +22 -0
- kintsugi-0.1.0.dist-info/WHEEL +4 -0
- kintsugi-0.1.0.dist-info/entry_points.txt +2 -0
- kintsugi-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Locale registry: region, script and register variants (data as code).
|
|
2
|
+
|
|
3
|
+
Each entry is a BCP-47-style code with its base language, ISO 15924 script, region,
|
|
4
|
+
and register (standard acrolect vs colloquial/creole/vernacular basilect). These are
|
|
5
|
+
red-team target locales; a model collapsing a variant to its base is exactly what the
|
|
6
|
+
attacks probe. The exhaustive MT-engine coverage catalogues live in docs, not here."""
|
|
7
|
+
|
|
8
|
+
LOCALES = [
|
|
9
|
+
{'code': 'pt-BR', 'name': 'Portuguese (Brazil)', 'base': 'pt', 'script': 'Latn', 'region': 'BR', 'register': 'standard', 'band': 'pt', 'note': ''},
|
|
10
|
+
{'code': 'pt-PT', 'name': 'Portuguese (Portugal)', 'base': 'pt', 'script': 'Latn', 'region': 'PT', 'register': 'standard', 'band': 'pt', 'note': ''},
|
|
11
|
+
{'code': 'pt-MO', 'name': 'Portuguese (Macau)', 'base': 'pt', 'script': 'Latn', 'region': 'MO', 'register': 'standard', 'band': 'pt', 'note': ''},
|
|
12
|
+
{'code': 'fr-FR', 'name': 'French (France)', 'base': 'fr', 'script': 'Latn', 'region': 'FR', 'register': 'standard', 'band': 'fr', 'note': ''},
|
|
13
|
+
{'code': 'fr-CA', 'name': 'French (Canada)', 'base': 'fr', 'script': 'Latn', 'region': 'CA', 'register': 'standard', 'band': 'fr', 'note': ''},
|
|
14
|
+
{'code': 'fr-CH', 'name': 'French (Switzerland)', 'base': 'fr', 'script': 'Latn', 'region': 'CH', 'register': 'standard', 'band': 'fr', 'note': ''},
|
|
15
|
+
{'code': 'fr-BE', 'name': 'French (Belgium)', 'base': 'fr', 'script': 'Latn', 'region': 'BE', 'register': 'standard', 'band': 'fr', 'note': ''},
|
|
16
|
+
{'code': 'de-DE', 'name': 'German (Germany)', 'base': 'de', 'script': 'Latn', 'region': 'DE', 'register': 'standard', 'band': 'de', 'note': ''},
|
|
17
|
+
{'code': 'de-AT', 'name': 'Austrian Standard German', 'base': 'de', 'script': 'Latn', 'region': 'AT', 'register': 'standard', 'band': 'de', 'note': ''},
|
|
18
|
+
{'code': 'de-CH', 'name': 'Swiss Standard German', 'base': 'de', 'script': 'Latn', 'region': 'CH', 'register': 'standard', 'band': 'de', 'note': 'written standard in Switzerland; distinct from spoken gsw'},
|
|
19
|
+
{'code': 'gsw', 'name': 'Swiss German (Schweizerdeutsch)', 'base': 'de', 'script': 'Latn', 'region': 'CH', 'register': 'vernacular', 'band': 'de', 'note': 'spoken Alemannic vernacular; models tend to collapse it to Standard German'},
|
|
20
|
+
{'code': 'es-ES', 'name': 'Spanish (Spain)', 'base': 'es', 'script': 'Latn', 'region': 'ES', 'register': 'standard', 'band': 'es', 'note': ''},
|
|
21
|
+
{'code': 'es-MX', 'name': 'Spanish (Mexico)', 'base': 'es', 'script': 'Latn', 'region': 'MX', 'register': 'standard', 'band': 'es', 'note': ''},
|
|
22
|
+
{'code': 'es-AR', 'name': 'Spanish (Argentina)', 'base': 'es', 'script': 'Latn', 'region': 'AR', 'register': 'standard', 'band': 'es', 'note': ''},
|
|
23
|
+
{'code': 'es-419', 'name': 'Spanish (Latin America)', 'base': 'es', 'script': 'Latn', 'region': '419', 'register': 'standard', 'band': 'es', 'note': ''},
|
|
24
|
+
{'code': 'es-US', 'name': 'Spanish (United States)', 'base': 'es', 'script': 'Latn', 'region': 'US', 'register': 'standard', 'band': 'es', 'note': ''},
|
|
25
|
+
{'code': 'en-US', 'name': 'English (United States)', 'base': 'en', 'script': 'Latn', 'region': 'US', 'register': 'standard', 'band': 'en', 'note': ''},
|
|
26
|
+
{'code': 'en-GB', 'name': 'English (United Kingdom)', 'base': 'en', 'script': 'Latn', 'region': 'GB', 'register': 'standard', 'band': 'en', 'note': ''},
|
|
27
|
+
{'code': 'en-AU', 'name': 'English (Australia)', 'base': 'en', 'script': 'Latn', 'region': 'AU', 'register': 'standard', 'band': 'en', 'note': ''},
|
|
28
|
+
{'code': 'en-CA', 'name': 'English (Canada)', 'base': 'en', 'script': 'Latn', 'region': 'CA', 'register': 'standard', 'band': 'en', 'note': ''},
|
|
29
|
+
{'code': 'en-IN', 'name': 'Indian Standard English', 'base': 'en', 'script': 'Latn', 'region': 'IN', 'register': 'standard', 'band': 'en', 'note': ''},
|
|
30
|
+
{'code': 'en-SG', 'name': 'Singapore Standard English', 'base': 'en', 'script': 'Latn', 'region': 'SG', 'register': 'standard', 'band': 'en', 'note': 'the acrolect; contrast with en-SG-x-singlish'},
|
|
31
|
+
{'code': 'en-SG-x-singlish', 'name': 'Singlish', 'base': 'en', 'script': 'Latn', 'region': 'SG', 'register': 'creole', 'band': 'en', 'note': "Singapore colloquial English creole; the basilect to en-SG's acrolect"},
|
|
32
|
+
{'code': 'en-IN-x-hinglish', 'name': 'Hinglish', 'base': 'en', 'script': 'Latn', 'region': 'IN', 'register': 'colloquial', 'band': 'en', 'note': 'Hindi-English code-mixing; romanised; contrast en-IN and hi-Deva'},
|
|
33
|
+
{'code': 'en-JM', 'name': 'Jamaican Standard English', 'base': 'en', 'script': 'Latn', 'region': 'JM', 'register': 'standard', 'band': 'en', 'note': ''},
|
|
34
|
+
{'code': 'jam', 'name': 'Jamaican Patois', 'base': 'en', 'script': 'Latn', 'region': 'JM', 'register': 'creole', 'band': 'en', 'note': 'English-lexified creole; the basilect to Jamaican Standard English'},
|
|
35
|
+
{'code': 'nl-NL', 'name': 'Dutch (Netherlands)', 'base': 'nl', 'script': 'Latn', 'region': 'NL', 'register': 'standard', 'band': 'nl', 'note': ''},
|
|
36
|
+
{'code': 'nl-BE', 'name': 'Dutch (Belgium / Flemish)', 'base': 'nl', 'script': 'Latn', 'region': 'BE', 'register': 'standard', 'band': 'nl', 'note': ''},
|
|
37
|
+
{'code': 'it-IT', 'name': 'Italian (Italy)', 'base': 'it', 'script': 'Latn', 'region': 'IT', 'register': 'standard', 'band': 'it', 'note': ''},
|
|
38
|
+
{'code': 'it-CH', 'name': 'Italian (Switzerland)', 'base': 'it', 'script': 'Latn', 'region': 'CH', 'register': 'standard', 'band': 'it', 'note': ''},
|
|
39
|
+
{'code': 'ar', 'name': 'Modern Standard Arabic', 'base': 'ar', 'script': 'Arab', 'region': None, 'register': 'standard', 'band': 'ar', 'note': 'MSA'},
|
|
40
|
+
{'code': 'ar-EG', 'name': 'Egyptian Arabic', 'base': 'ar', 'script': 'Arab', 'region': 'EG', 'register': 'vernacular', 'band': 'ar', 'note': 'spoken dialect; Arabic diglossia vs MSA'},
|
|
41
|
+
{'code': 'ar-MA', 'name': 'Moroccan Arabic (Darija)', 'base': 'ar', 'script': 'Arab', 'region': 'MA', 'register': 'vernacular', 'band': 'ar', 'note': 'spoken dialect; Arabic diglossia vs MSA'},
|
|
42
|
+
{'code': 'zh-Hans', 'name': 'Chinese (Simplified)', 'base': 'zh', 'script': 'Hans', 'region': None, 'register': 'standard', 'band': 'zh', 'note': ''},
|
|
43
|
+
{'code': 'zh-Hant', 'name': 'Chinese (Traditional)', 'base': 'zh', 'script': 'Hant', 'region': None, 'register': 'standard', 'band': 'zh-Hant', 'note': ''},
|
|
44
|
+
{'code': 'zh-CN', 'name': 'Chinese (Mainland, Simplified)', 'base': 'zh', 'script': 'Hans', 'region': 'CN', 'register': 'standard', 'band': 'zh', 'note': ''},
|
|
45
|
+
{'code': 'zh-SG', 'name': 'Chinese (Singapore, Simplified)', 'base': 'zh', 'script': 'Hans', 'region': 'SG', 'register': 'standard', 'band': 'zh', 'note': ''},
|
|
46
|
+
{'code': 'zh-TW', 'name': 'Chinese (Taiwan, Traditional)', 'base': 'zh', 'script': 'Hant', 'region': 'TW', 'register': 'standard', 'band': 'zh-Hant', 'note': ''},
|
|
47
|
+
{'code': 'zh-HK', 'name': 'Chinese (Hong Kong, Traditional)', 'base': 'zh', 'script': 'Hant', 'region': 'HK', 'register': 'standard', 'band': 'zh-Hant', 'note': 'written standard Chinese in HK; spoken form is Cantonese (yue)'},
|
|
48
|
+
{'code': 'zh-MO', 'name': 'Chinese (Macau, Traditional)', 'base': 'zh', 'script': 'Hant', 'region': 'MO', 'register': 'standard', 'band': 'zh-Hant', 'note': ''},
|
|
49
|
+
{'code': 'yue', 'name': 'Cantonese', 'base': 'yue', 'script': 'Hant', 'region': 'HK', 'register': 'vernacular', 'band': 'yue', 'note': 'spoken Cantonese; models tend to collapse it to Mandarin'},
|
|
50
|
+
{'code': 'hi-Deva', 'name': 'Hindi (Devanagari)', 'base': 'hi', 'script': 'Deva', 'region': 'IN', 'register': 'standard', 'band': 'hi', 'note': ''},
|
|
51
|
+
{'code': 'hi-Latn', 'name': 'Hindi (Latin / romanised)', 'base': 'hi', 'script': 'Latn', 'region': 'IN', 'register': 'colloquial', 'band': 'hi', 'note': 'romanised Hindi; overlaps Hinglish'},
|
|
52
|
+
{'code': 'sr-Cyrl', 'name': 'Serbian (Cyrillic)', 'base': 'sr', 'script': 'Cyrl', 'region': 'RS', 'register': 'standard', 'band': 'sr', 'note': ''},
|
|
53
|
+
{'code': 'sr-Latn', 'name': 'Serbian (Latin)', 'base': 'sr', 'script': 'Latn', 'region': 'RS', 'register': 'standard', 'band': 'sr', 'note': ''},
|
|
54
|
+
{'code': 'az-Latn', 'name': 'Azerbaijani (Latin)', 'base': 'az', 'script': 'Latn', 'region': 'AZ', 'register': 'standard', 'band': 'az', 'note': ''},
|
|
55
|
+
{'code': 'az-Cyrl', 'name': 'Azerbaijani (Cyrillic)', 'base': 'az', 'script': 'Cyrl', 'region': 'AZ', 'register': 'standard', 'band': 'az', 'note': ''},
|
|
56
|
+
{'code': 'uz-Latn', 'name': 'Uzbek (Latin)', 'base': 'uz', 'script': 'Latn', 'region': 'UZ', 'register': 'standard', 'band': 'uz', 'note': ''},
|
|
57
|
+
{'code': 'uz-Cyrl', 'name': 'Uzbek (Cyrillic)', 'base': 'uz', 'script': 'Cyrl', 'region': 'UZ', 'register': 'standard', 'band': 'uz', 'note': ''},
|
|
58
|
+
{'code': 'pa-Guru', 'name': 'Punjabi (Gurmukhi)', 'base': 'pa', 'script': 'Guru', 'region': 'IN', 'register': 'standard', 'band': 'pa', 'note': ''},
|
|
59
|
+
{'code': 'pa-Arab', 'name': 'Punjabi (Shahmukhi)', 'base': 'pa', 'script': 'Arab', 'region': 'PK', 'register': 'standard', 'band': 'pa', 'note': ''},
|
|
60
|
+
{'code': 'ms-Latn', 'name': 'Malay (Rumi / Latin)', 'base': 'ms', 'script': 'Latn', 'region': 'MY', 'register': 'standard', 'band': 'ms', 'note': ''},
|
|
61
|
+
{'code': 'ms-Arab', 'name': 'Malay (Jawi / Arabic)', 'base': 'ms', 'script': 'Arab', 'region': 'MY', 'register': 'standard', 'band': 'ms', 'note': ''},
|
|
62
|
+
{'code': 'kk-Cyrl', 'name': 'Kazakh (Cyrillic)', 'base': 'kk', 'script': 'Cyrl', 'region': 'KZ', 'register': 'standard', 'band': 'kk', 'note': ''},
|
|
63
|
+
{'code': 'kk-Latn', 'name': 'Kazakh (Latin)', 'base': 'kk', 'script': 'Latn', 'region': 'KZ', 'register': 'standard', 'band': 'kk', 'note': ''},
|
|
64
|
+
{'code': 'mn-Cyrl', 'name': 'Mongolian (Cyrillic)', 'base': 'mn', 'script': 'Cyrl', 'region': 'MN', 'register': 'standard', 'band': 'mn', 'note': ''},
|
|
65
|
+
{'code': 'mn-Mong', 'name': 'Mongolian (Traditional script)', 'base': 'mn', 'script': 'Mong', 'region': 'MN', 'register': 'standard', 'band': 'mn', 'note': ''},
|
|
66
|
+
]
|
kintsugi/attacks.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Attack categories and seeds discovered by the red-team project.
|
|
2
|
+
|
|
3
|
+
201 categories / 2822 seeds, loaded from ``data/attacks.json``. Each category
|
|
4
|
+
is either ``open-weights`` (found attacking local Ollama models) or ``frontier``
|
|
5
|
+
(found attacking a frontier API). Categories also carry the catastrophic/bad
|
|
6
|
+
rates actually observed in the runs, keyed by language pair.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Iterator, Optional
|
|
12
|
+
|
|
13
|
+
from ._data.attacks import CATEGORIES as _CATEGORIES
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Attack:
|
|
18
|
+
"""One adversarial source string plus where it came from."""
|
|
19
|
+
|
|
20
|
+
source: str
|
|
21
|
+
category: str
|
|
22
|
+
origin: str = "open-weights"
|
|
23
|
+
lang_pair: Optional[str] = None
|
|
24
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def categories() -> list[dict[str, Any]]:
|
|
28
|
+
"""All category records: name, description, origin, seeds, observed rates."""
|
|
29
|
+
return _CATEGORIES
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def category_names() -> list[str]:
|
|
33
|
+
return [c["name"] for c in categories()]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def observed_rates() -> dict[str, dict[str, dict[str, int]]]:
|
|
37
|
+
"""Per-category observed counts ``{category: {lang_pair: {n, catastrophic, bad}}}``.
|
|
38
|
+
|
|
39
|
+
Counts exclude rows polluted by judge rate-limit errors, so rates are honest.
|
|
40
|
+
"""
|
|
41
|
+
return {c["name"]: c["observed"] for c in categories() if "observed" in c}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_attacks(
|
|
45
|
+
category: Optional[str] = None,
|
|
46
|
+
origin: Optional[str] = None,
|
|
47
|
+
lang_pair: Optional[str] = None,
|
|
48
|
+
limit_per_category: Optional[int] = None,
|
|
49
|
+
) -> list[Attack]:
|
|
50
|
+
"""Expand categories into individual :class:`Attack` seeds.
|
|
51
|
+
|
|
52
|
+
Filter by ``category`` (exact name), ``origin`` ('open-weights'/'frontier'),
|
|
53
|
+
or a category's own ``lang_pair`` tag; cap seeds per category with
|
|
54
|
+
``limit_per_category``.
|
|
55
|
+
"""
|
|
56
|
+
out: list[Attack] = []
|
|
57
|
+
for cat in categories():
|
|
58
|
+
if category is not None and cat["name"] != category:
|
|
59
|
+
continue
|
|
60
|
+
if origin is not None and cat.get("origin") != origin:
|
|
61
|
+
continue
|
|
62
|
+
clp = cat.get("lang_pair")
|
|
63
|
+
if lang_pair is not None and clp is not None and clp != lang_pair:
|
|
64
|
+
continue
|
|
65
|
+
seeds = cat["seeds"]
|
|
66
|
+
if limit_per_category is not None:
|
|
67
|
+
seeds = seeds[:limit_per_category]
|
|
68
|
+
for s in seeds:
|
|
69
|
+
out.append(Attack(source=s, category=cat["name"], origin=cat.get("origin", "open-weights"),
|
|
70
|
+
lang_pair=clp, meta={"description": cat.get("description", "")}))
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def iter_attacks(**kwargs: Any) -> Iterator[Attack]:
|
|
75
|
+
yield from load_attacks(**kwargs)
|
kintsugi/cli.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Command-line interface for kintsugi.
|
|
2
|
+
|
|
3
|
+
kintsugi list [--origin open-weights|frontier]
|
|
4
|
+
kintsugi list-framings
|
|
5
|
+
kintsugi attack --target ollama:qwen3.5:0.8b --lang en-de --category prompt_injection
|
|
6
|
+
kintsugi frequency --target ollama:qwen3.5:0.8b --direction en-ja --n 30
|
|
7
|
+
kintsugi transform zero_width "Do not take with alcohol"
|
|
8
|
+
|
|
9
|
+
A target (and optional judge) is ``kind:model`` where kind is ``ollama``, ``hf``
|
|
10
|
+
or ``anthropic``. For your own model, use the Python API with CallableProvider.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from . import frequency, locales, transforms
|
|
20
|
+
from .attacks import categories, load_attacks
|
|
21
|
+
from .engine import LANG_PAIRS, RedTeam
|
|
22
|
+
from .framings import framings
|
|
23
|
+
from .providers import AnthropicProvider, HuggingFaceProvider, OllamaProvider, Provider
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _make_provider(spec: Optional[str]) -> Optional[Provider]:
|
|
27
|
+
if not spec:
|
|
28
|
+
return None
|
|
29
|
+
kind, _, model = spec.partition(":")
|
|
30
|
+
if not model:
|
|
31
|
+
raise SystemExit(f"target must be kind:model, got {spec!r}")
|
|
32
|
+
if kind == "ollama":
|
|
33
|
+
return OllamaProvider(model)
|
|
34
|
+
if kind in ("hf", "huggingface"):
|
|
35
|
+
return HuggingFaceProvider(model)
|
|
36
|
+
if kind == "anthropic":
|
|
37
|
+
return AnthropicProvider(model)
|
|
38
|
+
raise SystemExit(f"unknown provider kind {kind!r} (use ollama, hf, or anthropic)")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _cmd_list(args):
|
|
42
|
+
for c in categories():
|
|
43
|
+
if args.origin and c.get("origin") != args.origin:
|
|
44
|
+
continue
|
|
45
|
+
n = len(c["seeds"])
|
|
46
|
+
print(f"{c['name']:<48} {c.get('origin',''):<12} {n:>4} seeds {c['description'][:70]}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _cmd_list_framings(args):
|
|
50
|
+
for f in framings():
|
|
51
|
+
print(f"{f['name']:<40} batch {f.get('batch','?')} {f['description'][:80]}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _cmd_transform(args):
|
|
55
|
+
print(transforms.apply(args.name, args.text, seed=args.seed))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _run(args, attacks):
|
|
59
|
+
target = _make_provider(args.target)
|
|
60
|
+
judge = _make_provider(args.judge)
|
|
61
|
+
rt = RedTeam(target=target, judge=judge, lang_pair=args.lang)
|
|
62
|
+
findings = rt.run(attacks, progress=not args.quiet)
|
|
63
|
+
summary = rt.summary(findings)
|
|
64
|
+
print(json.dumps(summary, indent=1, ensure_ascii=False))
|
|
65
|
+
if args.out:
|
|
66
|
+
with open(args.out, "w", encoding="utf-8") as fh:
|
|
67
|
+
for f in findings:
|
|
68
|
+
fh.write(json.dumps(f.to_dict(), ensure_ascii=False) + "\n")
|
|
69
|
+
print(f"wrote {len(findings)} findings to {args.out}", file=sys.stderr)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _cmd_attack(args):
|
|
73
|
+
attacks = load_attacks(category=args.category, origin=args.origin,
|
|
74
|
+
limit_per_category=args.limit)
|
|
75
|
+
if not attacks:
|
|
76
|
+
raise SystemExit("no attacks matched those filters")
|
|
77
|
+
_run(args, attacks)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _cmd_list_locales(args):
|
|
81
|
+
for x in locales.locales(register=args.register, script=args.script):
|
|
82
|
+
reg = x["region"] or "-"
|
|
83
|
+
print(f"{x['code']:<22} {x['script']:<5} {reg:<4} {x['register']:<11} band={x['band']:<8} {x['name']}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _cmd_frequency(args):
|
|
87
|
+
attacks = frequency.probes(lang=args.lang_of, band=args.band, n=args.n, direction=args.direction)
|
|
88
|
+
# run in the probe's own language, not the attack default: words go source -> English
|
|
89
|
+
if args.lang_of == "ja":
|
|
90
|
+
args.lang = args.direction
|
|
91
|
+
else:
|
|
92
|
+
base = locales.band_key(args.lang_of) if locales.is_locale(args.lang_of) else args.lang_of
|
|
93
|
+
args.lang = f"{base}-en"
|
|
94
|
+
_run(args, attacks)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
98
|
+
p = argparse.ArgumentParser(prog="kintsugi", description="Red-team machine translation.")
|
|
99
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
100
|
+
|
|
101
|
+
pl = sub.add_parser("list", help="list attack categories")
|
|
102
|
+
pl.add_argument("--origin", choices=["open-weights", "frontier"])
|
|
103
|
+
pl.set_defaults(func=_cmd_list)
|
|
104
|
+
|
|
105
|
+
pf = sub.add_parser("list-framings", help="list translator framings")
|
|
106
|
+
pf.set_defaults(func=_cmd_list_framings)
|
|
107
|
+
|
|
108
|
+
ploc = sub.add_parser("list-locales", help="list region/script/register locale targets")
|
|
109
|
+
ploc.add_argument("--register", choices=["standard", "colloquial", "creole", "vernacular"])
|
|
110
|
+
ploc.add_argument("--script")
|
|
111
|
+
ploc.set_defaults(func=_cmd_list_locales)
|
|
112
|
+
|
|
113
|
+
pt = sub.add_parser("transform", help="apply an adversarial transform to text")
|
|
114
|
+
pt.add_argument("name")
|
|
115
|
+
pt.add_argument("text")
|
|
116
|
+
pt.add_argument("--seed", type=int)
|
|
117
|
+
pt.set_defaults(func=_cmd_transform)
|
|
118
|
+
|
|
119
|
+
def add_run_args(sp):
|
|
120
|
+
sp.add_argument("--target", required=True, help="kind:model, e.g. ollama:qwen3.5:0.8b")
|
|
121
|
+
sp.add_argument("--judge", help="kind:model for the LLM judge (optional)")
|
|
122
|
+
sp.add_argument("--lang", default="en-de", choices=sorted(LANG_PAIRS), help="language pair")
|
|
123
|
+
sp.add_argument("--out", help="write findings JSONL here")
|
|
124
|
+
sp.add_argument("--quiet", action="store_true")
|
|
125
|
+
|
|
126
|
+
pa = sub.add_parser("attack", help="run attack categories against a target")
|
|
127
|
+
add_run_args(pa)
|
|
128
|
+
pa.add_argument("--category", help="single category name (default: all)")
|
|
129
|
+
pa.add_argument("--origin", choices=["open-weights", "frontier"])
|
|
130
|
+
pa.add_argument("--limit", type=int, help="max seeds per category")
|
|
131
|
+
pa.set_defaults(func=_cmd_attack)
|
|
132
|
+
|
|
133
|
+
pq = sub.add_parser("frequency", help="frequent vs rare word probes (by language or locale)")
|
|
134
|
+
add_run_args(pq)
|
|
135
|
+
pq.add_argument("--lang-of", default="ja", dest="lang_of",
|
|
136
|
+
help="frequency language or locale code (e.g. ja, de, zh-Hant, en-SG-x-singlish)")
|
|
137
|
+
pq.add_argument("--direction", default="en-ja", choices=["en-ja", "ja-en"],
|
|
138
|
+
help="Japanese only: gloss->ja or ja->gloss")
|
|
139
|
+
pq.add_argument("--band", default="rare", choices=["frequent", "rare"])
|
|
140
|
+
pq.add_argument("--n", type=int, default=30)
|
|
141
|
+
pq.set_defaults(func=_cmd_frequency)
|
|
142
|
+
return p
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main(argv: Optional[list[str]] = None) -> None:
|
|
146
|
+
args = build_parser().parse_args(argv)
|
|
147
|
+
args.func(args)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
main()
|