switch-trust-cli 1.7.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.
- switch_trust/__init__.py +0 -0
- switch_trust/cli/__init__.py +0 -0
- switch_trust/cli/__main__.py +3 -0
- switch_trust/cli/builtin_config.json +879 -0
- switch_trust/cli/console.py +127 -0
- switch_trust/cli/eval_cli.py +905 -0
- switch_trust/cli/file_filter.py +172 -0
- switch_trust/cli/init_cli.py +156 -0
- switch_trust/cli/main.py +384 -0
- switch_trust/cli/output_formatters.py +482 -0
- switch_trust/cli/rich_observer.py +89 -0
- switch_trust/cli/runner.py +274 -0
- switch_trust/cli/scan_cli.py +314 -0
- switch_trust/cli/telemetry.py +430 -0
- switch_trust/cli/utils.py +158 -0
- switch_trust/cli/version.py +1 -0
- switch_trust/data/detector_prompts/llm01_adversarial.txt +59 -0
- switch_trust/data/detector_prompts/llm01_fixed.txt +6 -0
- switch_trust/data/detector_prompts/llm02_adversarial.txt +79 -0
- switch_trust/data/detector_prompts/llm02_fixed.txt +6 -0
- switch_trust/data/detector_prompts/llm05_adversarial.txt +67 -0
- switch_trust/data/detector_prompts/llm05_fixed.txt +14 -0
- switch_trust/data/detector_prompts/llm06_adversarial.txt +139 -0
- switch_trust/data/detector_prompts/llm06_fixed.txt +13 -0
- switch_trust/data/detector_prompts/llm07_adversarial.txt +189 -0
- switch_trust/data/detector_prompts/llm07_fixed.txt +12 -0
- switch_trust/data/detector_prompts/llm09_adversarial.txt +83 -0
- switch_trust/data/detector_prompts/llm09_fixed.txt +13 -0
- switch_trust/data/llm01_prompt_injection.csv +6708 -0
- switch_trust/data/llm02_sensitive_information.csv +1069 -0
- switch_trust/data/llm05_unsafe_output.csv +3882 -0
- switch_trust/data/llm06_excessive_agency.csv +778 -0
- switch_trust/data/llm07_system_prompt_leakage.csv +971 -0
- switch_trust/data/llm09_hallucination-goals.csv +32419 -0
- switch_trust/data/llm09_hallucination.csv +599 -0
- switch_trust/data/pii_leakage.csv +706 -0
- switch_trust/data/secret_leakage.csv +1380 -0
- switch_trust/eval/__init__.py +0 -0
- switch_trust/eval/common/__init__.py +0 -0
- switch_trust/eval/common/converter_anthropic.py +172 -0
- switch_trust/eval/common/converter_genai.py +171 -0
- switch_trust/eval/common/converter_openai.py +198 -0
- switch_trust/eval/common/log.py +116 -0
- switch_trust/eval/common/reference.py +25 -0
- switch_trust/eval/common/schema.py +190 -0
- switch_trust/eval/common/utils.py +119 -0
- switch_trust/eval/core/__init__.py +0 -0
- switch_trust/eval/core/detectors/__init__.py +0 -0
- switch_trust/eval/core/detectors/detector.py +25 -0
- switch_trust/eval/core/detectors/detector_garak.py +112 -0
- switch_trust/eval/core/detectors/detector_model.py +123 -0
- switch_trust/eval/core/detectors/detector_model_adversarial.py +91 -0
- switch_trust/eval/core/detectors/detector_pii.py +67 -0
- switch_trust/eval/core/detectors/detector_secret.py +88 -0
- switch_trust/eval/core/detectors/detector_topic_guard.py +63 -0
- switch_trust/eval/core/detectors/detector_toxicity.py +78 -0
- switch_trust/eval/core/eval/__init__.py +0 -0
- switch_trust/eval/core/eval/eval_creator.py +166 -0
- switch_trust/eval/core/eval/evaluation.py +112 -0
- switch_trust/eval/core/eval/evaluation_adversarial.py +412 -0
- switch_trust/eval/core/eval/evaluation_garak_module.py +45 -0
- switch_trust/eval/core/eval/evaluation_garak_probe.py +308 -0
- switch_trust/eval/core/eval/evaluation_message_list.py +46 -0
- switch_trust/eval/core/eval/evaluation_multi.py +276 -0
- switch_trust/eval/core/eval/evaluation_single.py +89 -0
- switch_trust/eval/core/eval/evaluation_single_prompt.py +62 -0
- switch_trust/eval/core/eval/evaluation_topic_guard.py +305 -0
- switch_trust/eval/core/eval/metric_conciseness.py +179 -0
- switch_trust/eval/core/eval/metric_factual_accuracy.py +312 -0
- switch_trust/eval/core/eval/metric_instruction_adherence.py +177 -0
- switch_trust/eval/core/eval/metric_tone.py +180 -0
- switch_trust/eval/core/eval/metric_toxicity.py +146 -0
- switch_trust/eval/core/eval/observer.py +137 -0
- switch_trust/eval/core/message/__init__.py +0 -0
- switch_trust/eval/core/message/message_collection.py +37 -0
- switch_trust/eval/core/message/message_collection_csv.py +44 -0
- switch_trust/eval/core/message/message_collection_garak.py +46 -0
- switch_trust/eval/core/message/message_collection_memory.py +34 -0
- switch_trust/eval/core/models/__init__.py +0 -0
- switch_trust/eval/core/models/generator_model.py +421 -0
- switch_trust/eval/core/models/model.py +172 -0
- switch_trust/eval/core/models/model_adk.py +186 -0
- switch_trust/eval/core/models/model_anthropic.py +79 -0
- switch_trust/eval/core/models/model_anthropic_agent.py +104 -0
- switch_trust/eval/core/models/model_gemini.py +146 -0
- switch_trust/eval/core/models/model_generic_http.py +120 -0
- switch_trust/eval/core/models/model_http.py +135 -0
- switch_trust/eval/core/models/model_huggingface.py +75 -0
- switch_trust/eval/core/models/model_langserve.py +96 -0
- switch_trust/eval/core/models/model_litellm.py +52 -0
- switch_trust/eval/core/models/model_ollama.py +65 -0
- switch_trust/eval/core/models/model_openai.py +71 -0
- switch_trust/eval/core/models/model_openai_agent.py +92 -0
- switch_trust/eval/core/models/model_openai_compatible.py +66 -0
- switch_trust/eval/core/models/model_per_endpoint_limiter.py +80 -0
- switch_trust/eval/core/models/model_retry.py +204 -0
- switch_trust/eval/core/models/model_sync_wrapper.py +38 -0
- switch_trust/eval/core/models/model_vertex_agent_runtime.py +315 -0
- switch_trust/eval/core/models/response_schema.py +148 -0
- switch_trust/eval/core/optional_deps.py +57 -0
- switch_trust/eval/db/__init__.py +0 -0
- switch_trust/eval/db/base/__init__.py +0 -0
- switch_trust/eval/db/base/common_types.py +16 -0
- switch_trust/eval/db/base/detectors/__init__.py +0 -0
- switch_trust/eval/db/base/detectors/detector_helpers.py +88 -0
- switch_trust/eval/db/base/detectors/detector_repository.py +96 -0
- switch_trust/eval/db/base/detectors/detector_types.py +77 -0
- switch_trust/eval/db/base/eval/__init__.py +0 -0
- switch_trust/eval/db/base/eval/eval_helpers.py +186 -0
- switch_trust/eval/db/base/eval/eval_repository.py +120 -0
- switch_trust/eval/db/base/eval/eval_run.py +142 -0
- switch_trust/eval/db/base/eval/eval_types.py +127 -0
- switch_trust/eval/db/base/eval/model_eval_repository.py +79 -0
- switch_trust/eval/db/base/eval/model_eval_run_repository.py +72 -0
- switch_trust/eval/db/base/eval/model_eval_run_result_repository.py +42 -0
- switch_trust/eval/db/base/eval/model_eval_run_result_types.py +21 -0
- switch_trust/eval/db/base/eval/model_eval_run_types.py +78 -0
- switch_trust/eval/db/base/eval/model_eval_types.py +69 -0
- switch_trust/eval/db/base/message/__init__.py +0 -0
- switch_trust/eval/db/base/message/message_collection_helpers.py +50 -0
- switch_trust/eval/db/base/message/message_collection_repository.py +95 -0
- switch_trust/eval/db/base/message/message_collection_types.py +87 -0
- switch_trust/eval/db/base/models/__init__.py +0 -0
- switch_trust/eval/db/base/models/model_helpers.py +253 -0
- switch_trust/eval/db/base/models/model_repository.py +104 -0
- switch_trust/eval/db/base/models/model_types.py +159 -0
- switch_trust/eval/db/json/__init__.py +0 -0
- switch_trust/eval/db/json/repository_json.py +559 -0
- switch_trust/scan/__init__.py +12 -0
- switch_trust/scan/agent_scanner.py +812 -0
- switch_trust/scan/config/agent_opengrep_rules.yaml +533 -0
- switch_trust/scan/config/agent_taxonomy.json +517 -0
- switch_trust/scan/config/agentic_cvss_mapping.yaml +136 -0
- switch_trust/scan/config/compliance_mappings.json +110 -0
- switch_trust/scan/config/triage_prompt.txt +326 -0
- switch_trust/scan/constants.py +15 -0
- switch_trust/scan/exception_classification.py +179 -0
- switch_trust/scan/llm_provider.py +330 -0
- switch_trust/scan/opengrep_resolver.py +12 -0
- switch_trust/scan/reasoner.py +602 -0
- switch_trust/scan/schema.py +315 -0
- switch_trust/scan/scorer.py +372 -0
- switch_trust/scan/static_scanner.py +681 -0
- switch_trust/scan/taxonomy.py +121 -0
- switch_trust/scan/tool_dispatcher.py +773 -0
- switch_trust/scan/trace_logger.py +118 -0
- switch_trust/scan/trace_logger_file.py +143 -0
- switch_trust/scan/trace_logger_log.py +100 -0
- switch_trust/scan/triage.py +614 -0
- switch_trust/schema.py +79 -0
- switch_trust/secret_anonymizer.py +72 -0
- switch_trust_cli-1.7.0.dist-info/METADATA +495 -0
- switch_trust_cli-1.7.0.dist-info/RECORD +157 -0
- switch_trust_cli-1.7.0.dist-info/WHEEL +5 -0
- switch_trust_cli-1.7.0.dist-info/entry_points.txt +2 -0
- switch_trust_cli-1.7.0.dist-info/licenses/LICENSE +210 -0
- switch_trust_cli-1.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Structured-output schema plumbing shared across model adapters.
|
|
2
|
+
|
|
3
|
+
Judge/generator callers pass a Pydantic ``BaseModel`` subclass as the
|
|
4
|
+
``output_schema`` argument to :meth:`Model.generate`. Each provider adapter
|
|
5
|
+
translates it to that provider's native structured-output mechanism:
|
|
6
|
+
|
|
7
|
+
- OpenAI / OpenAI-compatible (vLLM, Azure, ...) → ``json_schema`` response format
|
|
8
|
+
- Ollama → the native ``format`` JSON-schema parameter
|
|
9
|
+
- Gemini → ``response_mime_type`` + ``response_schema``
|
|
10
|
+
- LiteLLM → ``response_format`` (the Pydantic class, translated per provider)
|
|
11
|
+
- Anthropic → a single forced tool whose ``input_schema`` is the target schema
|
|
12
|
+
|
|
13
|
+
:func:`parse_model_response` deserialises the model output back into the schema,
|
|
14
|
+
with a best-effort fallback for the model-under-test and any provider that could
|
|
15
|
+
not hard-enforce the shape.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import re
|
|
22
|
+
from typing import Any, TypeVar
|
|
23
|
+
|
|
24
|
+
from pydantic import BaseModel, ValidationError
|
|
25
|
+
|
|
26
|
+
from switch_trust.eval.core.models.model import (
|
|
27
|
+
ModelResponse,
|
|
28
|
+
extract_text_from_message,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ModelResponseParseError(ValueError):
|
|
33
|
+
"""A model/judge response could not be parsed into the target schema.
|
|
34
|
+
|
|
35
|
+
A ``ValueError`` subclass, so any existing ``except ValueError`` handling
|
|
36
|
+
keeps working unchanged; the distinct type lets the worker's error
|
|
37
|
+
categorization (see ``platform/worker.py::_classify_error_category``)
|
|
38
|
+
distinguish this from other permanent failures for dashboard/log
|
|
39
|
+
discoverability.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
# TypeVar rather than PEP 695 `[T: BaseModel]` syntax: eval/core is exported to
|
|
46
|
+
# the flintai-cli wheel (requires-python >= 3.11), and PEP 695 type parameters
|
|
47
|
+
# are a syntax error before 3.12.
|
|
48
|
+
T = TypeVar("T", bound=BaseModel)
|
|
49
|
+
|
|
50
|
+
# Name of the forced tool used by providers that emit structured output via
|
|
51
|
+
# tool calling (Anthropic).
|
|
52
|
+
STRUCTURED_OUTPUT_TOOL_NAME = "emit_structured_output"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def to_json_schema(schema: type[BaseModel]) -> dict[str, Any]:
|
|
56
|
+
"""Return the plain JSON Schema for a Pydantic model."""
|
|
57
|
+
return schema.model_json_schema()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def to_strict_json_schema(schema: type[BaseModel]) -> dict[str, Any]:
|
|
61
|
+
"""Return a JSON Schema tightened for OpenAI-style strict decoding.
|
|
62
|
+
|
|
63
|
+
Strict ``json_schema`` response formats require every object to set
|
|
64
|
+
``additionalProperties: false`` and to list all of its properties in
|
|
65
|
+
``required``. Pydantic emits ``required`` but not ``additionalProperties``,
|
|
66
|
+
so we walk the schema and add what the strict validator expects.
|
|
67
|
+
"""
|
|
68
|
+
return _tighten(schema.model_json_schema())
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _tighten(node: Any) -> Any:
|
|
72
|
+
if isinstance(node, dict):
|
|
73
|
+
tightened = {key: _tighten(value) for key, value in node.items()}
|
|
74
|
+
if tightened.get("type") == "object" and "properties" in tightened:
|
|
75
|
+
tightened["additionalProperties"] = False
|
|
76
|
+
tightened["required"] = list(tightened["properties"].keys())
|
|
77
|
+
return tightened
|
|
78
|
+
if isinstance(node, list):
|
|
79
|
+
return [_tighten(item) for item in node]
|
|
80
|
+
return node
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def to_openai_response_format(schema: type[BaseModel]) -> dict[str, Any]:
|
|
84
|
+
"""Build an OpenAI / vLLM ``response_format`` for ``schema``."""
|
|
85
|
+
return {
|
|
86
|
+
"type": "json_schema",
|
|
87
|
+
"json_schema": {
|
|
88
|
+
"name": schema.__name__,
|
|
89
|
+
"schema": to_strict_json_schema(schema),
|
|
90
|
+
"strict": True,
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def to_anthropic_tool(
|
|
96
|
+
schema: type[BaseModel],
|
|
97
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
98
|
+
"""Build a forced-tool definition and ``tool_choice`` for ``schema``.
|
|
99
|
+
|
|
100
|
+
Anthropic has no cross-version ``response_format``, so forcing a single
|
|
101
|
+
tool whose ``input_schema`` is the target schema makes the model emit the
|
|
102
|
+
payload as the tool input, which the adapter reads back as JSON.
|
|
103
|
+
"""
|
|
104
|
+
tool = {
|
|
105
|
+
"name": STRUCTURED_OUTPUT_TOOL_NAME,
|
|
106
|
+
"description": (
|
|
107
|
+
"Record the final structured result. Always call this tool with "
|
|
108
|
+
"the result and nothing else."
|
|
109
|
+
),
|
|
110
|
+
"input_schema": to_json_schema(schema),
|
|
111
|
+
}
|
|
112
|
+
tool_choice = {"type": "tool", "name": STRUCTURED_OUTPUT_TOOL_NAME}
|
|
113
|
+
return tool, tool_choice
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def parse_model_response(response: ModelResponse, schema: type[T]) -> T: # noqa: UP047
|
|
117
|
+
"""Deserialise a model response into ``schema``.
|
|
118
|
+
|
|
119
|
+
Providers that hard-enforce the schema return exactly the JSON payload;
|
|
120
|
+
those that cannot may wrap it in prose or markdown fences, so we fall back
|
|
121
|
+
to extracting the first ``{...}`` block. Raises ``ValueError`` if nothing
|
|
122
|
+
parses.
|
|
123
|
+
"""
|
|
124
|
+
if response.message is None:
|
|
125
|
+
raise ModelResponseParseError("model returned no message to parse")
|
|
126
|
+
return parse_json_text(extract_text_from_message(response.message), schema)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def parse_json_text(text: str, schema: type[T]) -> T: # noqa: UP047
|
|
130
|
+
"""Parse ``text`` into ``schema``, tolerating fences and surrounding prose."""
|
|
131
|
+
stripped = text.strip()
|
|
132
|
+
try:
|
|
133
|
+
return schema.model_validate_json(stripped)
|
|
134
|
+
except ValidationError as first_error:
|
|
135
|
+
match = re.search(r"\{.*\}", stripped, re.DOTALL)
|
|
136
|
+
if match is not None:
|
|
137
|
+
try:
|
|
138
|
+
return schema.model_validate_json(match.group())
|
|
139
|
+
except ValidationError:
|
|
140
|
+
pass
|
|
141
|
+
logger.debug(
|
|
142
|
+
"Failed to parse %s from model output: %r",
|
|
143
|
+
schema.__name__,
|
|
144
|
+
text,
|
|
145
|
+
)
|
|
146
|
+
raise ModelResponseParseError(
|
|
147
|
+
f"model output did not match schema {schema.__name__}"
|
|
148
|
+
) from first_error
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Lazy imports for the optional ``full`` dependency extra.
|
|
2
|
+
|
|
3
|
+
The published ``switch-trust-cli`` package ships ``transformers[torch]`` and
|
|
4
|
+
``garak`` as an optional extra (``pip install switch-trust-cli[full]``) rather than
|
|
5
|
+
as core dependencies, because they are large (``torch`` alone is ~2 GB and
|
|
6
|
+
on Linux also pulls in Cuda). The helpers here import those packages on demand
|
|
7
|
+
and raise a friendly ``ImportError`` pointing at the extra when they are
|
|
8
|
+
missing, instead of the bare import error a user would otherwise see.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
_INSTALL_HINT = "Install it with: pip install switch-trust-cli[full]"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def require_transformers_pipeline() -> Callable[..., Any]:
|
|
21
|
+
"""Return ``transformers.pipeline``.
|
|
22
|
+
|
|
23
|
+
Raises ``ImportError`` with an install hint if ``transformers`` (part of
|
|
24
|
+
the optional ``full`` extra) is not installed.
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
from transformers import pipeline
|
|
28
|
+
except ImportError as e:
|
|
29
|
+
raise ImportError(
|
|
30
|
+
"transformers is required for HuggingFace models and toxicity "
|
|
31
|
+
f"detection. {_INSTALL_HINT}"
|
|
32
|
+
) from e
|
|
33
|
+
return pipeline
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def require_garak() -> ModuleType:
|
|
37
|
+
"""Return the top-level ``garak`` module.
|
|
38
|
+
|
|
39
|
+
The submodules the eval engine reaches into (``garak.attempt``,
|
|
40
|
+
``garak._plugins``, ``garak.generators.base``, ...) are imported here so
|
|
41
|
+
they are attribute-accessible on the returned module — a bare
|
|
42
|
+
``import garak`` does not guarantee that.
|
|
43
|
+
|
|
44
|
+
Raises ``ImportError`` with an install hint if ``garak`` (part of the
|
|
45
|
+
optional ``full`` extra) is not installed.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
import garak
|
|
49
|
+
import garak._config # noqa: F401
|
|
50
|
+
import garak._plugins # noqa: F401
|
|
51
|
+
import garak.attempt # noqa: F401
|
|
52
|
+
import garak.generators.base # noqa: F401
|
|
53
|
+
except ImportError as e:
|
|
54
|
+
raise ImportError(
|
|
55
|
+
f"garak is required for garak probes and detectors. {_INSTALL_HINT}"
|
|
56
|
+
) from e
|
|
57
|
+
return garak
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Types shared by every repository's `*_types` module.
|
|
2
|
+
|
|
3
|
+
Each entity keeps its own ``*SortField`` (the column set differs per table) and
|
|
4
|
+
its own ``*SortOrder`` default, but the direction is the same two values
|
|
5
|
+
everywhere, so it lives here rather than being restated per module.
|
|
6
|
+
|
|
7
|
+
The per-entity modules re-export what they use, so
|
|
8
|
+
``from ...detectors.detector_types import SortDirection`` keeps working.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SortDirection(str, Enum):
|
|
15
|
+
ASC = "asc"
|
|
16
|
+
DESC = "desc"
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from switch_trust.eval.core.detectors.detector import Detector
|
|
4
|
+
from switch_trust.eval.db.base.detectors.detector_types import (
|
|
5
|
+
DbDetector,
|
|
6
|
+
DetectorType,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_detector(
|
|
11
|
+
db_detector: DbDetector,
|
|
12
|
+
) -> Detector:
|
|
13
|
+
"""Create a Detector instance from a DbDetector."""
|
|
14
|
+
if db_detector.type == DetectorType.GARAK:
|
|
15
|
+
if not db_detector.detector_name:
|
|
16
|
+
raise ValueError("detector_name is required for GARAK detectors")
|
|
17
|
+
from switch_trust.eval.core.detectors.detector_garak import ( # noqa: PLC0415 - deferred import cost
|
|
18
|
+
GarakDetector,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return GarakDetector(db_detector.detector_name)
|
|
22
|
+
|
|
23
|
+
elif db_detector.type == DetectorType.MODEL:
|
|
24
|
+
from switch_trust.eval.core.detectors.detector_model import ( # noqa: PLC0415 - patched at source in tests
|
|
25
|
+
ModelDetector,
|
|
26
|
+
)
|
|
27
|
+
from switch_trust.eval.core.models.generator_model import ( # noqa: PLC0415 - patched at source in tests
|
|
28
|
+
get_generator_model,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
model = get_generator_model()
|
|
32
|
+
kwargs = {}
|
|
33
|
+
if db_detector.prompt is not None:
|
|
34
|
+
kwargs["prompt"] = db_detector.prompt
|
|
35
|
+
return ModelDetector(model=model, **kwargs)
|
|
36
|
+
|
|
37
|
+
elif db_detector.type == DetectorType.ADVERSARIAL_MODEL:
|
|
38
|
+
from switch_trust.eval.core.detectors.detector_model_adversarial import ( # noqa: PLC0415 - patched at source in tests
|
|
39
|
+
AdversarialModelDetector,
|
|
40
|
+
)
|
|
41
|
+
from switch_trust.eval.core.models.generator_model import ( # noqa: PLC0415 - patched at source in tests
|
|
42
|
+
get_generator_model,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
model = get_generator_model()
|
|
46
|
+
kwargs = {}
|
|
47
|
+
kwargs["include_conversation"] = db_detector.include_conversation
|
|
48
|
+
if db_detector.prompt is not None:
|
|
49
|
+
kwargs["prompt"] = db_detector.prompt
|
|
50
|
+
return AdversarialModelDetector(model=model, **kwargs)
|
|
51
|
+
|
|
52
|
+
elif db_detector.type == DetectorType.PII:
|
|
53
|
+
from switch_trust.eval.core.detectors.detector_pii import ( # noqa: PLC0415 - patched at source in tests
|
|
54
|
+
PIIDetector,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return PIIDetector()
|
|
58
|
+
|
|
59
|
+
elif db_detector.type == DetectorType.SECRET:
|
|
60
|
+
from switch_trust.eval.core.detectors.detector_secret import ( # noqa: PLC0415 - patched at source in tests
|
|
61
|
+
SecretDetector,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return SecretDetector()
|
|
65
|
+
|
|
66
|
+
elif db_detector.type == DetectorType.TOPIC_GUARD:
|
|
67
|
+
from switch_trust.eval.core.detectors.detector_topic_guard import ( # noqa: PLC0415 - patched at source in tests
|
|
68
|
+
TopicGuardDetector,
|
|
69
|
+
)
|
|
70
|
+
from switch_trust.eval.core.models.generator_model import ( # noqa: PLC0415 - patched at source in tests
|
|
71
|
+
get_generator_model,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if not db_detector.agent_objective and not db_detector.agent_instructions:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
"at least one of agent_objective or "
|
|
77
|
+
"agent_instructions is required for "
|
|
78
|
+
"TOPIC_GUARD detectors"
|
|
79
|
+
)
|
|
80
|
+
model = get_generator_model()
|
|
81
|
+
return TopicGuardDetector(
|
|
82
|
+
model=model,
|
|
83
|
+
agent_objective=db_detector.agent_objective,
|
|
84
|
+
agent_instructions=db_detector.agent_instructions,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
else:
|
|
88
|
+
raise ValueError(f"unknown detector type: {db_detector.type}")
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Abstract interface for a repository of Detectors.
|
|
3
|
+
|
|
4
|
+
A DetectorRepository manages detector configurations,
|
|
5
|
+
providing CRUD operations over them. Detectors are
|
|
6
|
+
always referenced by ID.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
|
|
13
|
+
from switch_trust.eval.core.detectors.detector import Detector
|
|
14
|
+
from switch_trust.eval.db.base.detectors.detector_types import (
|
|
15
|
+
DbDetector,
|
|
16
|
+
DetectorListView,
|
|
17
|
+
DetectorSortOrder,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DetectorRepository(ABC):
|
|
22
|
+
"""Repository that manages Detector configurations."""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def list(self) -> list[DbDetector]:
|
|
26
|
+
"""Return all detector configurations in the repository."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def search(
|
|
31
|
+
self,
|
|
32
|
+
query: str | None = None,
|
|
33
|
+
types: list[str] | None = None,
|
|
34
|
+
order: DetectorSortOrder | None = None,
|
|
35
|
+
offset: int = 0,
|
|
36
|
+
limit: int = 50,
|
|
37
|
+
) -> DetectorListView:
|
|
38
|
+
"""Search detectors with pagination and sorting."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def get(self, id: str) -> DbDetector:
|
|
43
|
+
"""Return the detector configuration with the given id.
|
|
44
|
+
|
|
45
|
+
Raises KeyError if the detector does not exist.
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def create(
|
|
51
|
+
self,
|
|
52
|
+
config: DbDetector,
|
|
53
|
+
) -> DbDetector:
|
|
54
|
+
"""Persist a new detector configuration.
|
|
55
|
+
|
|
56
|
+
Raises ValueError if a detector with that name already
|
|
57
|
+
exists.
|
|
58
|
+
"""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def update(
|
|
63
|
+
self,
|
|
64
|
+
config: DbDetector,
|
|
65
|
+
) -> DbDetector:
|
|
66
|
+
"""Replace an existing detector configuration.
|
|
67
|
+
|
|
68
|
+
The detector is identified by ``config.id``.
|
|
69
|
+
|
|
70
|
+
Raises KeyError if the detector does not exist.
|
|
71
|
+
"""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
def get_detector(self, id: str) -> Detector:
|
|
75
|
+
"""Return a Detector instance for the given id.
|
|
76
|
+
|
|
77
|
+
Raises KeyError if the detector does not exist.
|
|
78
|
+
"""
|
|
79
|
+
from switch_trust.eval.db.base.detectors.detector_helpers import ( # noqa: PLC0415 - deferred import cost
|
|
80
|
+
create_detector,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return create_detector(self.get(id))
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def delete(self, id: str) -> None:
|
|
87
|
+
"""Delete the detector with the given id.
|
|
88
|
+
|
|
89
|
+
Raises KeyError if the detector does not exist.
|
|
90
|
+
"""
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
def clear(self) -> None:
|
|
95
|
+
"""Remove all detectors from the repository."""
|
|
96
|
+
pass
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from dataclasses_json import dataclass_json
|
|
6
|
+
|
|
7
|
+
from switch_trust.eval.common.reference import Reference, ReferenceType
|
|
8
|
+
from switch_trust.eval.common.utils import datetime_config, generate_id, now_utc
|
|
9
|
+
from switch_trust.eval.db.base.common_types import SortDirection
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DetectorType(str, Enum):
|
|
13
|
+
"""Discriminator for the kind of Detector."""
|
|
14
|
+
|
|
15
|
+
GARAK = "garak"
|
|
16
|
+
MODEL = "model"
|
|
17
|
+
ADVERSARIAL_MODEL = "adversarial_model"
|
|
18
|
+
PII = "pii"
|
|
19
|
+
SECRET = "secret"
|
|
20
|
+
TOPIC_GUARD = "topic_guard"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass_json
|
|
24
|
+
@dataclass
|
|
25
|
+
class DbDetector:
|
|
26
|
+
"""JSON-serialisable description of a Detector."""
|
|
27
|
+
|
|
28
|
+
type: DetectorType
|
|
29
|
+
name: str
|
|
30
|
+
id: str = field(default_factory=generate_id)
|
|
31
|
+
created: datetime = field(
|
|
32
|
+
default_factory=now_utc,
|
|
33
|
+
metadata=datetime_config,
|
|
34
|
+
)
|
|
35
|
+
description: str | None = None
|
|
36
|
+
tags: dict[str, str] = field(default_factory=dict)
|
|
37
|
+
include_conversation: bool = False
|
|
38
|
+
|
|
39
|
+
# -- garak type --
|
|
40
|
+
detector_name: str | None = None
|
|
41
|
+
|
|
42
|
+
# -- model type --
|
|
43
|
+
prompt: str | None = None
|
|
44
|
+
|
|
45
|
+
# -- topic_guard type --
|
|
46
|
+
agent_objective: str | None = None
|
|
47
|
+
agent_instructions: str | None = None
|
|
48
|
+
|
|
49
|
+
def get_ref(self) -> Reference:
|
|
50
|
+
return Reference(
|
|
51
|
+
self.id,
|
|
52
|
+
self.name,
|
|
53
|
+
ReferenceType.DETECTOR,
|
|
54
|
+
self.description,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DetectorSortField(str, Enum):
|
|
59
|
+
NAME = "name"
|
|
60
|
+
CREATED = "created"
|
|
61
|
+
TYPE = "type"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass_json
|
|
65
|
+
@dataclass
|
|
66
|
+
class DetectorSortOrder:
|
|
67
|
+
field: DetectorSortField = DetectorSortField.NAME
|
|
68
|
+
direction: SortDirection = SortDirection.ASC
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass_json
|
|
72
|
+
@dataclass
|
|
73
|
+
class DetectorListView:
|
|
74
|
+
"""Paginated list of detector configurations."""
|
|
75
|
+
|
|
76
|
+
items: list[DbDetector] = field(default_factory=list)
|
|
77
|
+
total: int = 0
|
|
File without changes
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from switch_trust.eval.core.eval.evaluation import Evaluation
|
|
4
|
+
from switch_trust.eval.core.models.generator_model import get_generator_model
|
|
5
|
+
from switch_trust.eval.db.base.detectors.detector_repository import DetectorRepository
|
|
6
|
+
from switch_trust.eval.db.base.eval.eval_types import DbEvaluation, EvaluationType
|
|
7
|
+
from switch_trust.eval.db.base.message.message_collection_repository import (
|
|
8
|
+
MessageCollectionRepository,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_evaluation(
|
|
13
|
+
db_evaluation: DbEvaluation,
|
|
14
|
+
message_collection_repo: MessageCollectionRepository | None = None,
|
|
15
|
+
detector_repo: DetectorRepository | None = None,
|
|
16
|
+
) -> Evaluation:
|
|
17
|
+
"""Create an Evaluation instance from a DbEvaluation."""
|
|
18
|
+
if db_evaluation.type == EvaluationType.MESSAGE_COLLECTION:
|
|
19
|
+
if message_collection_repo is None:
|
|
20
|
+
raise ValueError(
|
|
21
|
+
"message_collection_repo is required for MESSAGE_COLLECTION evaluations"
|
|
22
|
+
)
|
|
23
|
+
if db_evaluation.message_collection_id is None:
|
|
24
|
+
raise ValueError("message_collection_id must be set on the DbEvaluation")
|
|
25
|
+
if detector_repo is None:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
"detector_repo is required for MESSAGE_COLLECTION evaluations"
|
|
28
|
+
)
|
|
29
|
+
if db_evaluation.detector_id is None:
|
|
30
|
+
raise ValueError("detector_id must be set on the DbEvaluation")
|
|
31
|
+
from switch_trust.eval.core.eval.evaluation_message_list import ( # noqa: PLC0415 - patched at source in tests
|
|
32
|
+
MessageListEvaluation,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
message_collection = message_collection_repo.get_message_collection(
|
|
36
|
+
db_evaluation.message_collection_id
|
|
37
|
+
)
|
|
38
|
+
messages = message_collection.load()
|
|
39
|
+
detector = detector_repo.get_detector(
|
|
40
|
+
db_evaluation.detector_id,
|
|
41
|
+
)
|
|
42
|
+
return MessageListEvaluation(
|
|
43
|
+
messages=messages,
|
|
44
|
+
detector=detector,
|
|
45
|
+
num_prompts=db_evaluation.num_prompts,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
elif db_evaluation.type == EvaluationType.GARAK_PROBE:
|
|
49
|
+
from switch_trust.eval.core.eval.evaluation_garak_probe import ( # noqa: PLC0415 - patched at source in tests
|
|
50
|
+
GarakProbeEvaluation,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if db_evaluation.probe_name is None:
|
|
54
|
+
raise ValueError("probe_name must be set on the DbEvaluation")
|
|
55
|
+
return GarakProbeEvaluation(
|
|
56
|
+
probe_name=db_evaluation.probe_name,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
elif db_evaluation.type == EvaluationType.GARAK_MODULE:
|
|
60
|
+
from switch_trust.eval.core.eval.evaluation_garak_module import ( # noqa: PLC0415 - patched at source in tests
|
|
61
|
+
GarakModuleEvaluation,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if db_evaluation.module_name is None:
|
|
65
|
+
raise ValueError("module_name must be set on the DbEvaluation")
|
|
66
|
+
return GarakModuleEvaluation(
|
|
67
|
+
module_name=db_evaluation.module_name,
|
|
68
|
+
probe_names=db_evaluation.probe_names,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
elif db_evaluation.type == EvaluationType.METRIC_TOXICITY:
|
|
72
|
+
from switch_trust.eval.core.eval.metric_toxicity import ( # noqa: PLC0415 - patched at source in tests
|
|
73
|
+
ToxicityMetricEvaluation,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return ToxicityMetricEvaluation()
|
|
77
|
+
|
|
78
|
+
elif db_evaluation.type == EvaluationType.METRIC_CONCISENESS:
|
|
79
|
+
from switch_trust.eval.core.eval.metric_conciseness import ( # noqa: PLC0415 - patched at source in tests
|
|
80
|
+
ConcisenessMetricEvaluation,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return ConcisenessMetricEvaluation(
|
|
84
|
+
judge_model=get_generator_model(),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
elif db_evaluation.type == EvaluationType.METRIC_FACTUAL_ACCURACY:
|
|
88
|
+
from switch_trust.eval.core.eval.metric_factual_accuracy import ( # noqa: PLC0415 - patched at source in tests
|
|
89
|
+
FactualAccuracyMetricEvaluation,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return FactualAccuracyMetricEvaluation(
|
|
93
|
+
judge_model=get_generator_model(),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
elif db_evaluation.type == EvaluationType.METRIC_INSTRUCTION_ADHERENCE:
|
|
97
|
+
from switch_trust.eval.core.eval.metric_instruction_adherence import ( # noqa: PLC0415 - patched at source in tests
|
|
98
|
+
InstructionAdherenceMetricEvaluation,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return InstructionAdherenceMetricEvaluation(
|
|
102
|
+
judge_model=get_generator_model(),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
elif db_evaluation.type == EvaluationType.METRIC_TONE:
|
|
106
|
+
from switch_trust.eval.core.eval.metric_tone import ( # noqa: PLC0415 - patched at source in tests
|
|
107
|
+
ToneMetricEvaluation,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return ToneMetricEvaluation(
|
|
111
|
+
judge_model=get_generator_model(),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
elif db_evaluation.type == EvaluationType.ADVERSARIAL_PROBE:
|
|
115
|
+
from switch_trust.eval.core.eval.evaluation_adversarial import ( # noqa: PLC0415 - patched at source in tests
|
|
116
|
+
AdversarialEvaluation,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
goals: list[str] = []
|
|
120
|
+
if db_evaluation.message_collection_id and message_collection_repo:
|
|
121
|
+
mc = message_collection_repo.get_message_collection(
|
|
122
|
+
db_evaluation.message_collection_id,
|
|
123
|
+
)
|
|
124
|
+
messages = mc.load()
|
|
125
|
+
goals = [
|
|
126
|
+
part.text
|
|
127
|
+
for msg in messages
|
|
128
|
+
for part in msg.content.parts
|
|
129
|
+
if part.text is not None
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
if db_evaluation.adversarial_goals:
|
|
133
|
+
goals.extend(db_evaluation.adversarial_goals)
|
|
134
|
+
|
|
135
|
+
if not goals:
|
|
136
|
+
raise ValueError(
|
|
137
|
+
"adversarial_goals or "
|
|
138
|
+
"message_collection_id must be set on "
|
|
139
|
+
"the DbEvaluation"
|
|
140
|
+
)
|
|
141
|
+
if not db_evaluation.attack_techniques:
|
|
142
|
+
raise ValueError(
|
|
143
|
+
"attack_techniques must be set on "
|
|
144
|
+
"the DbEvaluation for adversarial probes"
|
|
145
|
+
)
|
|
146
|
+
if detector_repo is None:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
"detector_repo is required for ADVERSARIAL_PROBE evaluations"
|
|
149
|
+
)
|
|
150
|
+
if not db_evaluation.detector_id:
|
|
151
|
+
raise ValueError(
|
|
152
|
+
"detector_id must be set on the DbEvaluation for adversarial probes"
|
|
153
|
+
)
|
|
154
|
+
detector = detector_repo.get_detector(
|
|
155
|
+
db_evaluation.detector_id,
|
|
156
|
+
)
|
|
157
|
+
return AdversarialEvaluation(
|
|
158
|
+
goals=goals,
|
|
159
|
+
attack_techniques=db_evaluation.attack_techniques,
|
|
160
|
+
detector=detector,
|
|
161
|
+
num_prompts=db_evaluation.num_prompts or 5,
|
|
162
|
+
max_turns=db_evaluation.max_turns or 5,
|
|
163
|
+
attacker_model=get_generator_model(),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
elif db_evaluation.type == EvaluationType.TOPIC_GUARD:
|
|
167
|
+
from switch_trust.eval.core.eval.evaluation_topic_guard import ( # noqa: PLC0415 - patched at source in tests
|
|
168
|
+
TopicGuardEvaluation,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if not db_evaluation.agent_objective and not db_evaluation.agent_instructions:
|
|
172
|
+
raise ValueError(
|
|
173
|
+
"at least one of agent_objective or "
|
|
174
|
+
"agent_instructions must be set on "
|
|
175
|
+
"the DbEvaluation"
|
|
176
|
+
)
|
|
177
|
+
return TopicGuardEvaluation(
|
|
178
|
+
agent_objective=db_evaluation.agent_objective,
|
|
179
|
+
agent_instructions=db_evaluation.agent_instructions,
|
|
180
|
+
num_prompts=db_evaluation.num_prompts or 5,
|
|
181
|
+
max_turns=db_evaluation.max_turns or 5,
|
|
182
|
+
attacker_model=get_generator_model(),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
else:
|
|
186
|
+
raise ValueError(f"unknown evaluation type: {db_evaluation.type}")
|