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.
Files changed (157) hide show
  1. switch_trust/__init__.py +0 -0
  2. switch_trust/cli/__init__.py +0 -0
  3. switch_trust/cli/__main__.py +3 -0
  4. switch_trust/cli/builtin_config.json +879 -0
  5. switch_trust/cli/console.py +127 -0
  6. switch_trust/cli/eval_cli.py +905 -0
  7. switch_trust/cli/file_filter.py +172 -0
  8. switch_trust/cli/init_cli.py +156 -0
  9. switch_trust/cli/main.py +384 -0
  10. switch_trust/cli/output_formatters.py +482 -0
  11. switch_trust/cli/rich_observer.py +89 -0
  12. switch_trust/cli/runner.py +274 -0
  13. switch_trust/cli/scan_cli.py +314 -0
  14. switch_trust/cli/telemetry.py +430 -0
  15. switch_trust/cli/utils.py +158 -0
  16. switch_trust/cli/version.py +1 -0
  17. switch_trust/data/detector_prompts/llm01_adversarial.txt +59 -0
  18. switch_trust/data/detector_prompts/llm01_fixed.txt +6 -0
  19. switch_trust/data/detector_prompts/llm02_adversarial.txt +79 -0
  20. switch_trust/data/detector_prompts/llm02_fixed.txt +6 -0
  21. switch_trust/data/detector_prompts/llm05_adversarial.txt +67 -0
  22. switch_trust/data/detector_prompts/llm05_fixed.txt +14 -0
  23. switch_trust/data/detector_prompts/llm06_adversarial.txt +139 -0
  24. switch_trust/data/detector_prompts/llm06_fixed.txt +13 -0
  25. switch_trust/data/detector_prompts/llm07_adversarial.txt +189 -0
  26. switch_trust/data/detector_prompts/llm07_fixed.txt +12 -0
  27. switch_trust/data/detector_prompts/llm09_adversarial.txt +83 -0
  28. switch_trust/data/detector_prompts/llm09_fixed.txt +13 -0
  29. switch_trust/data/llm01_prompt_injection.csv +6708 -0
  30. switch_trust/data/llm02_sensitive_information.csv +1069 -0
  31. switch_trust/data/llm05_unsafe_output.csv +3882 -0
  32. switch_trust/data/llm06_excessive_agency.csv +778 -0
  33. switch_trust/data/llm07_system_prompt_leakage.csv +971 -0
  34. switch_trust/data/llm09_hallucination-goals.csv +32419 -0
  35. switch_trust/data/llm09_hallucination.csv +599 -0
  36. switch_trust/data/pii_leakage.csv +706 -0
  37. switch_trust/data/secret_leakage.csv +1380 -0
  38. switch_trust/eval/__init__.py +0 -0
  39. switch_trust/eval/common/__init__.py +0 -0
  40. switch_trust/eval/common/converter_anthropic.py +172 -0
  41. switch_trust/eval/common/converter_genai.py +171 -0
  42. switch_trust/eval/common/converter_openai.py +198 -0
  43. switch_trust/eval/common/log.py +116 -0
  44. switch_trust/eval/common/reference.py +25 -0
  45. switch_trust/eval/common/schema.py +190 -0
  46. switch_trust/eval/common/utils.py +119 -0
  47. switch_trust/eval/core/__init__.py +0 -0
  48. switch_trust/eval/core/detectors/__init__.py +0 -0
  49. switch_trust/eval/core/detectors/detector.py +25 -0
  50. switch_trust/eval/core/detectors/detector_garak.py +112 -0
  51. switch_trust/eval/core/detectors/detector_model.py +123 -0
  52. switch_trust/eval/core/detectors/detector_model_adversarial.py +91 -0
  53. switch_trust/eval/core/detectors/detector_pii.py +67 -0
  54. switch_trust/eval/core/detectors/detector_secret.py +88 -0
  55. switch_trust/eval/core/detectors/detector_topic_guard.py +63 -0
  56. switch_trust/eval/core/detectors/detector_toxicity.py +78 -0
  57. switch_trust/eval/core/eval/__init__.py +0 -0
  58. switch_trust/eval/core/eval/eval_creator.py +166 -0
  59. switch_trust/eval/core/eval/evaluation.py +112 -0
  60. switch_trust/eval/core/eval/evaluation_adversarial.py +412 -0
  61. switch_trust/eval/core/eval/evaluation_garak_module.py +45 -0
  62. switch_trust/eval/core/eval/evaluation_garak_probe.py +308 -0
  63. switch_trust/eval/core/eval/evaluation_message_list.py +46 -0
  64. switch_trust/eval/core/eval/evaluation_multi.py +276 -0
  65. switch_trust/eval/core/eval/evaluation_single.py +89 -0
  66. switch_trust/eval/core/eval/evaluation_single_prompt.py +62 -0
  67. switch_trust/eval/core/eval/evaluation_topic_guard.py +305 -0
  68. switch_trust/eval/core/eval/metric_conciseness.py +179 -0
  69. switch_trust/eval/core/eval/metric_factual_accuracy.py +312 -0
  70. switch_trust/eval/core/eval/metric_instruction_adherence.py +177 -0
  71. switch_trust/eval/core/eval/metric_tone.py +180 -0
  72. switch_trust/eval/core/eval/metric_toxicity.py +146 -0
  73. switch_trust/eval/core/eval/observer.py +137 -0
  74. switch_trust/eval/core/message/__init__.py +0 -0
  75. switch_trust/eval/core/message/message_collection.py +37 -0
  76. switch_trust/eval/core/message/message_collection_csv.py +44 -0
  77. switch_trust/eval/core/message/message_collection_garak.py +46 -0
  78. switch_trust/eval/core/message/message_collection_memory.py +34 -0
  79. switch_trust/eval/core/models/__init__.py +0 -0
  80. switch_trust/eval/core/models/generator_model.py +421 -0
  81. switch_trust/eval/core/models/model.py +172 -0
  82. switch_trust/eval/core/models/model_adk.py +186 -0
  83. switch_trust/eval/core/models/model_anthropic.py +79 -0
  84. switch_trust/eval/core/models/model_anthropic_agent.py +104 -0
  85. switch_trust/eval/core/models/model_gemini.py +146 -0
  86. switch_trust/eval/core/models/model_generic_http.py +120 -0
  87. switch_trust/eval/core/models/model_http.py +135 -0
  88. switch_trust/eval/core/models/model_huggingface.py +75 -0
  89. switch_trust/eval/core/models/model_langserve.py +96 -0
  90. switch_trust/eval/core/models/model_litellm.py +52 -0
  91. switch_trust/eval/core/models/model_ollama.py +65 -0
  92. switch_trust/eval/core/models/model_openai.py +71 -0
  93. switch_trust/eval/core/models/model_openai_agent.py +92 -0
  94. switch_trust/eval/core/models/model_openai_compatible.py +66 -0
  95. switch_trust/eval/core/models/model_per_endpoint_limiter.py +80 -0
  96. switch_trust/eval/core/models/model_retry.py +204 -0
  97. switch_trust/eval/core/models/model_sync_wrapper.py +38 -0
  98. switch_trust/eval/core/models/model_vertex_agent_runtime.py +315 -0
  99. switch_trust/eval/core/models/response_schema.py +148 -0
  100. switch_trust/eval/core/optional_deps.py +57 -0
  101. switch_trust/eval/db/__init__.py +0 -0
  102. switch_trust/eval/db/base/__init__.py +0 -0
  103. switch_trust/eval/db/base/common_types.py +16 -0
  104. switch_trust/eval/db/base/detectors/__init__.py +0 -0
  105. switch_trust/eval/db/base/detectors/detector_helpers.py +88 -0
  106. switch_trust/eval/db/base/detectors/detector_repository.py +96 -0
  107. switch_trust/eval/db/base/detectors/detector_types.py +77 -0
  108. switch_trust/eval/db/base/eval/__init__.py +0 -0
  109. switch_trust/eval/db/base/eval/eval_helpers.py +186 -0
  110. switch_trust/eval/db/base/eval/eval_repository.py +120 -0
  111. switch_trust/eval/db/base/eval/eval_run.py +142 -0
  112. switch_trust/eval/db/base/eval/eval_types.py +127 -0
  113. switch_trust/eval/db/base/eval/model_eval_repository.py +79 -0
  114. switch_trust/eval/db/base/eval/model_eval_run_repository.py +72 -0
  115. switch_trust/eval/db/base/eval/model_eval_run_result_repository.py +42 -0
  116. switch_trust/eval/db/base/eval/model_eval_run_result_types.py +21 -0
  117. switch_trust/eval/db/base/eval/model_eval_run_types.py +78 -0
  118. switch_trust/eval/db/base/eval/model_eval_types.py +69 -0
  119. switch_trust/eval/db/base/message/__init__.py +0 -0
  120. switch_trust/eval/db/base/message/message_collection_helpers.py +50 -0
  121. switch_trust/eval/db/base/message/message_collection_repository.py +95 -0
  122. switch_trust/eval/db/base/message/message_collection_types.py +87 -0
  123. switch_trust/eval/db/base/models/__init__.py +0 -0
  124. switch_trust/eval/db/base/models/model_helpers.py +253 -0
  125. switch_trust/eval/db/base/models/model_repository.py +104 -0
  126. switch_trust/eval/db/base/models/model_types.py +159 -0
  127. switch_trust/eval/db/json/__init__.py +0 -0
  128. switch_trust/eval/db/json/repository_json.py +559 -0
  129. switch_trust/scan/__init__.py +12 -0
  130. switch_trust/scan/agent_scanner.py +812 -0
  131. switch_trust/scan/config/agent_opengrep_rules.yaml +533 -0
  132. switch_trust/scan/config/agent_taxonomy.json +517 -0
  133. switch_trust/scan/config/agentic_cvss_mapping.yaml +136 -0
  134. switch_trust/scan/config/compliance_mappings.json +110 -0
  135. switch_trust/scan/config/triage_prompt.txt +326 -0
  136. switch_trust/scan/constants.py +15 -0
  137. switch_trust/scan/exception_classification.py +179 -0
  138. switch_trust/scan/llm_provider.py +330 -0
  139. switch_trust/scan/opengrep_resolver.py +12 -0
  140. switch_trust/scan/reasoner.py +602 -0
  141. switch_trust/scan/schema.py +315 -0
  142. switch_trust/scan/scorer.py +372 -0
  143. switch_trust/scan/static_scanner.py +681 -0
  144. switch_trust/scan/taxonomy.py +121 -0
  145. switch_trust/scan/tool_dispatcher.py +773 -0
  146. switch_trust/scan/trace_logger.py +118 -0
  147. switch_trust/scan/trace_logger_file.py +143 -0
  148. switch_trust/scan/trace_logger_log.py +100 -0
  149. switch_trust/scan/triage.py +614 -0
  150. switch_trust/schema.py +79 -0
  151. switch_trust/secret_anonymizer.py +72 -0
  152. switch_trust_cli-1.7.0.dist-info/METADATA +495 -0
  153. switch_trust_cli-1.7.0.dist-info/RECORD +157 -0
  154. switch_trust_cli-1.7.0.dist-info/WHEEL +5 -0
  155. switch_trust_cli-1.7.0.dist-info/entry_points.txt +2 -0
  156. switch_trust_cli-1.7.0.dist-info/licenses/LICENSE +210 -0
  157. 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}")