Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Guard cli run with invalid or incomplete config #677

Merged
merged 6 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions garak/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

version = -1 # eh why this is here? hm. who references it

system_params = "verbose narrow_output parallel_requests parallel_attempts".split()
system_params = (
"verbose narrow_output parallel_requests parallel_attempts skip_unknown".split()
)
run_params = "seed deprefix eval_threshold generations probe_tags interactive".split()
plugins_params = "model_type model_name extended_detectors".split()
reporting_params = "taxonomy report_prefix".split()
Expand Down Expand Up @@ -158,11 +160,12 @@ def load_config(

def parse_plugin_spec(
spec: str, category: str, probe_tag_filter: str = ""
) -> List[str]:
) -> tuple[List[str], List[str]]:
leondz marked this conversation as resolved.
Show resolved Hide resolved
from garak._plugins import enumerate_plugins

if spec is None or spec.lower() in ("", "auto", "none"):
return []
return [], []
unknown_plugins = []
leondz marked this conversation as resolved.
Show resolved Hide resolved
if spec.lower() in ("all", "*"):
plugin_names = [
name
Expand All @@ -173,13 +176,26 @@ def parse_plugin_spec(
plugin_names = []
for clause in spec.split(","):
if clause.count(".") < 1:
plugin_names += [
found_plugins = [
p
for p, a in enumerate_plugins(category=category)
if p.startswith(f"{category}.{clause}.") and a is True
]
if len(found_plugins) > 0:
plugin_names += found_plugins
else:
unknown_plugins += [clause]
else:
plugin_names += [f"{category}.{clause}"] # spec parsing
# validate the class exists
found_plugins = [
p
for p, a in enumerate_plugins(category=category)
if p == f"{category}.{clause}"
]
if len(found_plugins) > 0:
plugin_names += found_plugins
else:
unknown_plugins += [clause]

if probe_tag_filter is not None and len(probe_tag_filter) > 1:
plugins_to_skip = []
Expand All @@ -196,4 +212,4 @@ def parse_plugin_spec(
for plugin_to_skip in plugins_to_skip:
plugin_names.remove(plugin_to_skip)

return plugin_names
return plugin_names, unknown_plugins
72 changes: 51 additions & 21 deletions garak/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def main(arguments=[]) -> None:
import garak.command as command
import logging
import re
from colorama import Fore, Style

command.start_logging()
_config.load_base_config()
Expand Down Expand Up @@ -67,6 +68,11 @@ def main(arguments=[]) -> None:
default=_config.system.parallel_attempts,
help="How many probe attempts to launch in parallel.",
)
parser.add_argument(
"--skip_unknown",
action="store_true",
help="allow skip of unknown probes, detectors, or buffs",
)

## RUN
parser.add_argument(
Expand Down Expand Up @@ -327,9 +333,6 @@ def main(arguments=[]) -> None:
import garak.evaluators

try:
if not args.version and not args.report:
command.start_run()

leondz marked this conversation as resolved.
Show resolved Hide resolved
# do a special thing for CLIprobe options, generator options
if "probe_options" in args or "probe_option_file" in args:
if "probe_options" in args:
Expand Down Expand Up @@ -382,11 +385,14 @@ def main(arguments=[]) -> None:
from garak.interactive import interactive_mode

try:
command.start_run() # start run to track actions
interactive_mode()
except Exception as e:
logging.error(e)
print(e)
sys.exit(1)
finally:
command.end_run()

if args.version:
pass
Expand Down Expand Up @@ -429,7 +435,31 @@ def main(arguments=[]) -> None:
message = f"⚠️ Model type '{_config.plugins.model_type}' also needs a model name\n You can set one with e.g. --model_name \"billwurtz/gpt-1.0\""
logging.error(message)
raise ValueError(message)
print(f"📜 reporting to {_config.transient.report_filename}")

parsable_specs = ["probe", "detector", "buff"]
parsed_specs = {}
for spec_type in parsable_specs:
spec_namespace = f"{spec_type}s"
config_spec = getattr(_config.plugins, f"{spec_type}_spec", "")
config_tags = getattr(_config.run, f"{spec_type}_tags", "")
names, rejected = _config.parse_plugin_spec(
config_spec, spec_namespace, config_tags
)
parsed_specs[spec_type] = names
if rejected is not None and len(rejected) > 0:
if hasattr(args, "skip_unknown"): # attribute only set when True
header = f"Unknown {spec_namespace}:"
skip_msg = Fore.LIGHTYELLOW_EX + "SKIP" + Style.RESET_ALL
msg = f"{Fore.LIGHTYELLOW_EX}{header}\n" + "\n".join(
[f"{skip_msg} {spec}" for spec in rejected]
)
logging.warning(f"{header} " + ",".join(rejected))
print(msg)
else:
msg_list = ",".join(rejected)
raise ValueError(f"❌Unknown {spec_namespace}❌: {msg_list}")

evaluator = garak.evaluators.ThresholdEvaluator(_config.run.eval_threshold)

generator_module_name = _config.plugins.model_type.split(".")[0]
generator_mod = importlib.import_module(
Expand All @@ -445,9 +475,6 @@ def main(arguments=[]) -> None:
else:
generator_class_name = _config.plugins.model_type.split(".")[1]

# if 'model_name' not in args:
# generator = getattr(generator_mod, generator_class_name)()
# else:
generator = getattr(generator_mod, generator_class_name)(
_config.plugins.model_name
)
Expand All @@ -472,22 +499,20 @@ def main(arguments=[]) -> None:
)
autodan_generate(generator=generator, prompt=prompt, target=target)

probe_names = _config.parse_plugin_spec(
_config.plugins.probe_spec, "probes", _config.run.probe_tags
)
detector_names = _config.parse_plugin_spec(
_config.plugins.detector_spec, "detectors"
)
buff_names = _config.parse_plugin_spec(_config.plugins.buff_spec, "buffs")

evaluator = garak.evaluators.ThresholdEvaluator(_config.run.eval_threshold)

if detector_names == []:
command.probewise_run(generator, probe_names, evaluator, buff_names)
command.start_run() # start the run now that all config validation is complete
print(f"📜 reporting to {_config.transient.report_filename}")

if parsed_specs["detector"] == []:
command.probewise_run(
generator, parsed_specs["probe"], evaluator, parsed_specs["buff"]
)
else:
command.pxd_run(
generator, probe_names, detector_names, evaluator, buff_names
generator,
parsed_specs["probe"],
parsed_specs["detector"],
evaluator,
parsed_specs["buff"],
)

command.end_run()
Expand All @@ -499,4 +524,9 @@ def main(arguments=[]) -> None:
)
logging.info("nothing to do 🤷")
except KeyboardInterrupt:
print("User cancel received, terminating all runs")
msg = "User cancel received, terminating all runs"
logging.info(msg)
print(msg)
except ValueError as e:
logging.error(e)
print(e)
14 changes: 8 additions & 6 deletions garak/harnesses/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,18 @@ def run(self, model, probes, detectors, evaluator, announce_probe=True) -> None:
:type announce_probe: bool, optional
"""
if not detectors:
logging.warning("No detectors, nothing to do")
msg = "No detectors, nothing to do"
logging.warning(msg)
if hasattr(_config.system, "verbose") and _config.system.verbose >= 2:
print("No detectors, nothing to do")
return None
print(msg)
raise ValueError(msg)

if not probes:
logging.warning("No probes, nothing to do")
msg = "No probes, nothing to do"
logging.warning(msg)
if hasattr(_config.system, "verbose") and _config.system.verbose >= 2:
print("No probes, nothing to do")
return None
print(msg)
raise ValueError(msg)

for probe in probes:
logging.debug("harness: probe start for %s", probe.probename)
Expand Down
7 changes: 4 additions & 3 deletions garak/harnesses/probewise.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ def run(self, model, probenames, evaluator, buff_names=[]):
"""

if not probenames:
logging.warning("No probes, nothing to do")
msg = "No probes, nothing to do"
logging.warning(msg)
if hasattr(_config.system, "verbose") and _config.system.verbose >= 2:
print("No probes, nothing to do")
return None
print(msg)
raise ValueError(msg)

self._load_buffs(buff_names)

Expand Down
1 change: 1 addition & 0 deletions tests/cli/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def test_run_all_active_detectors(capsys):
"-g",
"1",
"--narrow_output",
"--skip_unknown",
]
)
result = capsys.readouterr()
Expand Down
60 changes: 43 additions & 17 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,18 +439,42 @@ def test_blank_generator_instance_loads_cli_config():

# test parsing of probespec
def test_probespec_loading():
assert _config.parse_plugin_spec(None, "detectors") == []
assert _config.parse_plugin_spec("Auto", "probes") == []
assert _config.parse_plugin_spec("NONE", "probes") == []
assert _config.parse_plugin_spec("", "generators") == []
assert _config.parse_plugin_spec("atkgen", "probes") == ["probes.atkgen.Tox"]
assert _config.parse_plugin_spec(None, "detectors") == ([], [])
assert _config.parse_plugin_spec("", "generators") == ([], [])
assert _config.parse_plugin_spec("Auto", "probes") == ([], [])
assert _config.parse_plugin_spec("NONE", "probes") == ([], [])
# reject unmatched spec entires
assert _config.parse_plugin_spec("probedoesnotexist", "probes") == (
[],
["probedoesnotexist"],
)
assert _config.parse_plugin_spec("atkgen,probedoesnotexist", "probes") == (
["probes.atkgen.Tox"],
["probedoesnotexist"],
)
assert _config.parse_plugin_spec("atkgen.Tox,probedoesnotexist", "probes") == (
["probes.atkgen.Tox"],
["probedoesnotexist"],
)
# reject unmatched spec entires for unknown class
assert _config.parse_plugin_spec(
"atkgen.Tox,atkgen.ProbeDoesNotExist", "probes"
) == (["probes.atkgen.Tox"], ["atkgen.ProbeDoesNotExist"])
# accept known disabled class
assert _config.parse_plugin_spec("dan.DanInTheWild", "probes") == (
["probes.dan.DanInTheWild"],
[],
)
# gather all class entires for namespace
assert _config.parse_plugin_spec("atkgen", "probes") == (["probes.atkgen.Tox"], [])
assert _config.parse_plugin_spec("always", "detectors") == (
["detectors.always.Fail", "detectors.always.Pass"],
[],
)
# reject all unknown class entires for namespace
assert _config.parse_plugin_spec(
"long.test.class,another.long.test.class", "probes"
) == ["probes.long.test.class", "probes.another.long.test.class"]
assert _config.parse_plugin_spec("always", "detectors") == [
"detectors.always.Fail",
"detectors.always.Pass",
]
) == ([], ["long.test.class", "another.long.test.class"])


def test_buff_config_assertion():
Expand All @@ -464,16 +488,18 @@ def test_buff_config_assertion():


def test_tag_filter():
assert (
_config.parse_plugin_spec("atkgen", "probes", probe_tag_filter="LOL NULL") == []
)
assert _config.parse_plugin_spec("*", "probes", probe_tag_filter="avid") != []
assert (
_config.parse_plugin_spec("all", "probes", probe_tag_filter="owasp:llm") != []
assert _config.parse_plugin_spec(
"atkgen", "probes", probe_tag_filter="LOL NULL"
) == ([], [])
assert _config.parse_plugin_spec("*", "probes", probe_tag_filter="avid") != ([], [])
assert _config.parse_plugin_spec("all", "probes", probe_tag_filter="owasp:llm") != (
[],
[],
)
assert "probes.lmrc.SexualContent" in _config.parse_plugin_spec(
found, rejected = _config.parse_plugin_spec(
"all", "probes", probe_tag_filter="risk-cards:lmrc:sexual_content"
)
assert "probes.lmrc.SexualContent" in found


def test_report_prefix_with_hitlog_no_explode():
Expand Down
Loading