Skip to content

Commit 755b777

Browse files
committed
Feature: multilingual machine translation (#943)
2 parents 4af0f3c + 786f5a4 commit 755b777

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2018
-156
lines changed

docs/source/configurable.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ such as ``show_100_pass_modules``.
107107
* ``eval_threshold`` - At what point in the 0..1 range output by detectors does a result count as a successful attack / hit
108108
* ``user_agent`` - What HTTP user agent string should garak use? ``{version}`` can be used to signify where garak version ID should go
109109
* ``soft_probe_prompt_cap`` - For probes that auto-scale their prompt count, the preferred limit of prompts per probe
110+
* ``target_lang`` - A single bcp47 value the target application for LLM accepts as prompt and output
111+
* ``translators`` - A list of configurations representing translators for converting from probe bcp47 language to land_spec target bcp47 languages
110112

111113
``plugins`` config items
112114
""""""""""""""""""""""""

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Code reference
6161
command
6262
exception
6363
interactive
64+
langservice
6465
payloads
6566
_config
6667
_plugins

docs/source/langservice.rst

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
The ``langservice`` module in garak is designed to handle text language tasks using various translation services and models.
2+
It provides an entry point to translation support backed by translators, each implementing different translation strategies and models, including both cloud-based services,
3+
like `DeepL<https://www.deepl.com/>`_ and `NVIDIA Riva<https://build.nvidia.com/nvidia/megatron-1b-nmt>`_, and local models like facebook/m2m100 available on `Hugging Face<https://huggingface.co/>`_.
4+
5+
garak.langservice
6+
=================
7+
8+
.. automodule:: garak.langservice
9+
:members:
10+
:undoc-members:
11+
:show-inheritance:
12+
13+
Translation support
14+
===================
15+
16+
This module provides translation support for probe and detector keywords and triggers.
17+
Allowing testing of models that accept and produce text in languages other than the language the plugin was written for.
18+
19+
* limitations:
20+
- This functionality is strongly coupled to ``bcp47`` code "en" for sentence detection and structure at this time.
21+
- Reverse translation is required for snowball probes, and Huggingface detectors due to model load formats.
22+
- Huggingface detectors primarily load English models. Requiring a target language NLI model for the detector.
23+
- If probes or detectors fail to load, you need may need to choose a smaller local translation model or utilize a remote service.
24+
- Translation may add significant execution time to the run depending on resources available.
25+
26+
Supported translation services
27+
------------------------------
28+
29+
- Huggingface
30+
- This project supports usage of the following translation models:
31+
- `Helsinki-NLP/opus-mt-{<source_lang>-<target_lang>} <https://huggingface.co/docs/transformers/model_doc/marian>`_
32+
- `facebook/m2m100_418M <https://huggingface.co/facebook/m2m100_418M>`_
33+
- `facebook/m2m100_1.2B <https://huggingface.co/facebook/m2m100_1.2B>`_
34+
- `DeepL <https://www.deepl.com/docs-api>`_
35+
- `NVIDIA Riva <https://build.nvidia.com/nvidia/megatron-1b-nmt>`_
36+
37+
API KEY Requirements
38+
--------------------
39+
40+
To use use DeepL API or Riva API to translate probe and detector keywords and triggers from cloud services an API key must be supplied.
41+
42+
API keys for the preferred service can be obtained in following locations:
43+
- `DeepL <https://www.deepl.com/en/pro-api>`_
44+
- `Riva <https://build.nvidia.com/nvidia/megatron-1b-nmt>`_
45+
46+
Supported languages for remote services:
47+
- `DeepL <https://developers.deepl.com/docs/resources/supported-languages>`_
48+
- `Riva <https://docs.nvidia.com/nim/riva/nmt/latest/getting-started.html#supported-languages>`_
49+
50+
API keys can be stored in environment variables with the following commands:
51+
52+
DeepL
53+
~~~~~
54+
55+
.. code-block:: bash
56+
57+
export DEEPL_API_KEY=xxxx
58+
59+
RIVA
60+
~~~
61+
62+
.. code-block:: bash
63+
64+
export RIVA_API_KEY=xxxx
65+
66+
Configuration file
67+
------------------
68+
69+
Translation function is configured in the ``run`` section of a configuration with the following keys:
70+
71+
target_lang - A single ``bcp47`` entry designating the language of the target under test. "ja", "fr", "jap" etc.
72+
translators - A list of language pair designated translator configurations.
73+
74+
* Note: The `Helsinki-NLP/opus-mt-{source},{target}` case uses different language formats. The language codes used to name models are inconsistent.
75+
Two-digit codes can usually be found `here<https://developers.google.com/admin-sdk/directory/v1/languages>`_, while three-digit codes require
76+
a search such as “language code {code}". More details can be found `here <https://github.com/Helsinki-NLP/OPUS-MT-train/tree/master/models>`_.
77+
78+
A translator configuration is provided using the project's configurable pattern with the following required keys:
79+
80+
* ``language`` - A ``,`` separated pair of ``bcp47`` entires describing translation format provided by the configuration
81+
* ``model_type`` - the module and optional instance class to be instantiated. local, remote, remote.DeeplTranslator etc.
82+
* ``model_name`` - (optional) the model name loaded for translation, required for ``local`` translator model_type
83+
84+
(Optional) Model specific parameters defined by the translator model type may exist.
85+
86+
* Note: local translation support loads a model and is not designed to support crossing the multi-processing boundary.
87+
88+
The translator configuration can be written to a file and the path passed, with the ``--config`` cli option.
89+
90+
An example template is provided below.
91+
92+
.. code-block:: yaml
93+
run:
94+
target_lang: {target language code}
95+
translators:
96+
- language: {source language code},{target language code}
97+
api_key: {your API key}
98+
model_type: {translator module or module.classname}
99+
model_name: {huggingface model name}
100+
- language: {target language code},{source language code}
101+
api_key: {your API key}
102+
model_type: {translator module or module.classname}
103+
model_name: {huggingface model name}
104+
105+
* Note: each translator is configured for a single translation pair and specification is required in each direction for a run to proceed.
106+
107+
Examples for translation configuration
108+
--------------------------------------
109+
110+
DeepL
111+
~~~~~
112+
113+
To use DeepL translation in garak, run the following command:
114+
You use the following yaml config.
115+
116+
.. code-block:: yaml
117+
run:
118+
target_lang: {target language code}
119+
translators:
120+
- language: {source language code},{target language code}
121+
model_type: remote.DeeplTranslator
122+
- language: {target language code},{source language code}
123+
model_type: remote.DeeplTranslator
124+
125+
126+
.. code-block:: bash
127+
128+
export DEEPL_API_KEY=xxxx
129+
python3 -m garak --model_type nim --model_name meta/llama-3.1-8b-instruct --probes encoding --config {path to your yaml config file}
130+
131+
132+
Riva
133+
~~~~
134+
135+
For Riva, run the following command:
136+
You use the following yaml config.
137+
138+
.. code-block:: yaml
139+
140+
run:
141+
target_lang: {target language code}
142+
translators:
143+
- language: {source language code},{target language code}
144+
model_type: remote
145+
- language: {target language code},{source language code}
146+
model_type: remote
147+
148+
149+
.. code-block:: bash
150+
151+
export RIVA_API_KEY=xxxx
152+
python3 -m garak --model_type nim --model_name meta/llama-3.1-8b-instruct --probes encoding --config {path to your yaml config file}
153+
154+
155+
Local
156+
~~~~~
157+
158+
For local translation, use the following command:
159+
You use the following yaml config.
160+
161+
.. code-block:: yaml
162+
run:
163+
target_lang: jap
164+
translators:
165+
- language: en,jap
166+
model_type: local
167+
- language: jap,en
168+
model_type: local
169+
170+
.. code-block:: bash
171+
172+
python3 -m garak --model_type nim --model_name meta/llama-3.1-8b-instruct --probes encoding --config {path to your yaml config file}
173+
174+
The default configuration will load `Helsinki-NLP MarianMT <https://huggingface.co/docs/transformers/model_doc/marian>`_ models for local translation.
175+
176+
Additional support for Huggingface ``M2M100Model`` type only is enabled by providing ``model_name`` for local translators. The model name provided must
177+
contain ``m2m100`` to be loaded by garak.
178+
179+
.. code-block:: yaml
180+
run:
181+
target_lang: ja
182+
translators:
183+
- language: en,ja
184+
model_type: local
185+
model_name: facebook/m2m100_418M
186+
- language: jap,en
187+
model_type: local
188+
model_name: facebook/m2m100_418M
189+
190+
191+
.. code-block:: bash
192+
193+
python3 -m garak --model_type nim --model_name meta/llama-3.1-8b-instruct --probes encoding --config {path to your yaml config file}

garak/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Top-level package for garak"""
22

3-
__version__ = "0.10.3.post1"
3+
__version__ = "0.11.0.pre1"
44
__app__ = "garak"
55
__description__ = "LLM vulnerability scanner"
66

garak/_config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ def _nested_dict():
115115
# this is so popular, let's set a default. what other defaults are worth setting? what's the policy?
116116
run.seed = None
117117
run.soft_probe_prompt_cap = 64
118+
run.target_lang = "en"
119+
run.translators = []
118120

119121
# placeholder
120122
# generator, probe, detector, buff = {}, {}, {}, {}

garak/_plugins.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -373,9 +373,14 @@ def load_plugin(path, break_on_fail=True, config_root=_config) -> object:
373373
match len(parts):
374374
case 2:
375375
category, module_name = parts
376-
generator_mod = importlib.import_module(
377-
f"garak.{category}.{module_name}"
378-
)
376+
try:
377+
generator_mod = importlib.import_module(
378+
f"garak.{category}.{module_name}"
379+
)
380+
except ModuleNotFoundError as e:
381+
raise ValueError(
382+
f"Unknown plugin module specification: {category}.{module_name}"
383+
) from e
379384
if generator_mod.DEFAULT_CLASS:
380385
plugin_class_name = generator_mod.DEFAULT_CLASS
381386
else:
@@ -391,7 +396,7 @@ def load_plugin(path, break_on_fail=True, config_root=_config) -> object:
391396
except ValueError as ve:
392397
if break_on_fail:
393398
raise ValueError(
394-
f'Expected plugin name in format category.module_name.class_name, got "{path}"'
399+
f'Expected plugin name in format category.module_name.class_name or category.module_name, got "{path}"'
395400
) from ve
396401
else:
397402
return False

garak/attempt.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ class Attempt:
3838
:type seq: int
3939
:param messages: conversation turn histories; list of list of dicts have the format {"role": role, "content": text}, with actor being something like "system", "user", "assistant"
4040
:type messages: List(dict)
41+
:param bcp47: Language code for prompt as sent to the target
42+
:type bcp47: str
43+
:param reverse_translator_outputs: The reverse translation of output based on the original language of the probe
44+
:param reverse_translator_outputs: List(str)
4145
4246
Expected use
4347
* an attempt tracks a seed prompt and responses to it
@@ -72,6 +76,8 @@ def __init__(
7276
detector_results=None,
7377
goal=None,
7478
seq=-1,
79+
bcp47=None, # language code for prompt as sent to the target
80+
reverse_translator_outputs=None,
7581
) -> None:
7682
self.uuid = uuid.uuid4()
7783
self.messages = []
@@ -86,6 +92,10 @@ def __init__(
8692
self.seq = seq
8793
if prompt is not None:
8894
self.prompt = prompt
95+
self.bcp47 = bcp47
96+
self.reverse_translator_outputs = (
97+
{} if reverse_translator_outputs is None else reverse_translator_outputs
98+
)
8999

90100
def as_dict(self) -> dict:
91101
"""Converts the attempt to a dictionary."""
@@ -103,6 +113,8 @@ def as_dict(self) -> dict:
103113
"notes": self.notes,
104114
"goal": self.goal,
105115
"messages": self.messages,
116+
"bcp47": self.bcp47,
117+
"reverse_translator_outputs": list(self.reverse_translator_outputs),
106118
}
107119

108120
@property
@@ -189,6 +201,37 @@ def latest_prompts(self, value):
189201
assert isinstance(value, list)
190202
self._add_turn("user", value)
191203

204+
def prompt_for(self, lang) -> str:
205+
"""prompt for a known language
206+
207+
When "*" or None are passed returns the prompt passed to the model
208+
"""
209+
if (
210+
lang is not None
211+
and self.bcp47 != "*"
212+
and lang != "*"
213+
and self.bcp47 != lang
214+
):
215+
return self.notes.get(
216+
"pre_translation_prompt", self.prompt
217+
) # update if found in notes
218+
219+
return self.prompt
220+
221+
def outputs_for(self, lang) -> List[str]:
222+
"""outputs for a known language
223+
224+
When "*" or None are passed returns the original model output
225+
"""
226+
if (
227+
lang is not None
228+
and self.bcp47 != "*"
229+
and lang != "*"
230+
and self.bcp47 != lang
231+
):
232+
return self.reverse_translator_outputs
233+
return self.all_outputs
234+
192235
def _expand_prompt_to_histories(self, breadth):
193236
"""expand a prompt-only message history to many threads"""
194237
if len(self.messages) == 0:

garak/detectors/base.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Base detectors.
22
3-
These detectors show expectations and operations of any detector implemented
3+
These detectors show expectations and operations of any detector implemented
44
in `garak`. `garak` detectors must inherit from a class in here."""
55

66
import logging
@@ -135,10 +135,9 @@ def detect(self, attempt: garak.attempt.Attempt) -> List[float]:
135135
# goal: skip None outputs
136136
# don't adjust attempt.outputs
137137

138-
non_none_outputs = [
139-
v for k, v in enumerate(attempt.all_outputs) if v is not None
140-
]
141-
# non_none_offsets = [k for k,v in enumerate(attempt.all_outputs) if v is not None] # we'll want this when outputs+scores need to align
138+
all_outputs = attempt.outputs_for(self.bcp47)
139+
non_none_outputs = [v for k, v in enumerate(all_outputs) if v is not None]
140+
# non_none_offsets = [k for k,v in enumerate(all_outputs) if v is not None] # we'll want this when outputs+scores need to align
142141
try:
143142
detector_raw_results = self.detector(
144143
non_none_outputs, **self.tokenizer_kwargs
@@ -179,10 +178,12 @@ def detect(
179178
self, attempt: garak.attempt.Attempt, case_sensitive=False
180179
) -> Iterable[float]:
181180
detector_results = []
182-
for output in attempt.all_outputs:
183-
match = False
181+
all_outputs = attempt.outputs_for(self.bcp47)
182+
183+
for output in all_outputs:
184184
if output is None:
185185
continue
186+
match = False
186187
for s in self.substrings:
187188
if not case_sensitive:
188189
s, output = s.lower(), output.lower()
@@ -212,14 +213,15 @@ def detect(
212213
self, attempt: garak.attempt.Attempt, case_sensitive=False
213214
) -> Iterable[float]:
214215
detector_results = []
216+
all_outputs = attempt.outputs_for(self.bcp47)
217+
215218
if "triggers" in attempt.notes:
216219
triggers = attempt.notes["triggers"]
217220
if isinstance(triggers, str):
218221
triggers = [triggers]
219-
for output in attempt.all_outputs:
222+
for output in all_outputs:
220223
if output is None:
221224
continue # trigger is absent
222-
223225
match = False
224226
for trigger in triggers:
225227
if case_sensitive:

garak/detectors/encoding.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Encoding detectors
22
3-
Detectors for assessing how well a given payload from probes.encoding is
3+
Detectors for assessing how well a given payload from probes.encoding is
44
output by the model
55
"""
66

0 commit comments

Comments
 (0)