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,69 @@
|
|
|
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
|
+
@dataclass_json
|
|
13
|
+
@dataclass
|
|
14
|
+
class DbModelEvaluation:
|
|
15
|
+
"""Links a Model to an Evaluation with a weight."""
|
|
16
|
+
|
|
17
|
+
model_id: str
|
|
18
|
+
evaluation_id: str
|
|
19
|
+
name: str
|
|
20
|
+
id: str = field(default_factory=generate_id)
|
|
21
|
+
created: datetime = field(
|
|
22
|
+
default_factory=now_utc,
|
|
23
|
+
metadata=datetime_config,
|
|
24
|
+
)
|
|
25
|
+
description: str | None = None
|
|
26
|
+
tags: dict[str, str] = field(default_factory=dict)
|
|
27
|
+
weight: float = 0.5
|
|
28
|
+
|
|
29
|
+
def get_ref(self) -> Reference:
|
|
30
|
+
return Reference(
|
|
31
|
+
self.id,
|
|
32
|
+
self.name,
|
|
33
|
+
ReferenceType.MODEL_EVALUATION_SUITE,
|
|
34
|
+
self.description,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ModelEvaluationSortField(str, Enum):
|
|
39
|
+
NAME = "name"
|
|
40
|
+
CREATED = "created"
|
|
41
|
+
WEIGHT = "weight"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass_json
|
|
45
|
+
@dataclass
|
|
46
|
+
class ModelEvaluationSortOrder:
|
|
47
|
+
field: ModelEvaluationSortField = ModelEvaluationSortField.NAME
|
|
48
|
+
direction: SortDirection = SortDirection.ASC
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass_json
|
|
52
|
+
@dataclass
|
|
53
|
+
class ModelEvaluationListItem:
|
|
54
|
+
"""A model-evaluation assignment with resolved references."""
|
|
55
|
+
|
|
56
|
+
config: DbModelEvaluation
|
|
57
|
+
model_ref: Reference | None = None
|
|
58
|
+
evaluation_ref: Reference | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass_json
|
|
62
|
+
@dataclass
|
|
63
|
+
class ModelEvaluationListView:
|
|
64
|
+
"""Paginated list of model-evaluation assignments."""
|
|
65
|
+
|
|
66
|
+
items: list[ModelEvaluationListItem] = field(
|
|
67
|
+
default_factory=list,
|
|
68
|
+
)
|
|
69
|
+
total: int = 0
|
|
File without changes
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from switch_trust.eval.common.schema import Message
|
|
2
|
+
from switch_trust.eval.core.message.message_collection import MessageCollection
|
|
3
|
+
from switch_trust.eval.db.base.message.message_collection_types import (
|
|
4
|
+
DbMessageCollection,
|
|
5
|
+
MessageCollectionType,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_message_collection(db_collection: DbMessageCollection) -> MessageCollection:
|
|
10
|
+
"""Create a MessageCollection instance from a DbMessageCollection."""
|
|
11
|
+
if db_collection.type == MessageCollectionType.IN_MEMORY:
|
|
12
|
+
from switch_trust.eval.core.message.message_collection_memory import ( # noqa: PLC0415 - mirrors the eval/db original
|
|
13
|
+
InMemoryMessageCollection,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if db_collection.prompts:
|
|
17
|
+
return InMemoryMessageCollection().from_strings(
|
|
18
|
+
db_collection.prompts,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
messages = None
|
|
22
|
+
if db_collection.messages:
|
|
23
|
+
messages = [Message.from_dict(m) for m in db_collection.messages]
|
|
24
|
+
return InMemoryMessageCollection(messages=messages)
|
|
25
|
+
|
|
26
|
+
elif db_collection.type == MessageCollectionType.CSV:
|
|
27
|
+
from switch_trust.eval.core.message.message_collection_csv import ( # noqa: PLC0415 - mirrors the eval/db original
|
|
28
|
+
CsvMessageCollection,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if not db_collection.filename:
|
|
32
|
+
raise ValueError("filename is required for csv collections")
|
|
33
|
+
return CsvMessageCollection(
|
|
34
|
+
filename=db_collection.filename,
|
|
35
|
+
column=db_collection.column or "prompt",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
elif db_collection.type == MessageCollectionType.GARAK:
|
|
39
|
+
from switch_trust.eval.core.message.message_collection_garak import ( # noqa: PLC0415 - mirrors the eval/db original
|
|
40
|
+
GarakMessageCollection,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if not db_collection.probe_name:
|
|
44
|
+
raise ValueError("probe_name is required for garak collections")
|
|
45
|
+
return GarakMessageCollection(
|
|
46
|
+
probe_name=db_collection.probe_name,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
else:
|
|
50
|
+
raise ValueError(f"unknown collection type: {db_collection.type}")
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Abstract interface for a repository of MessageCollections.
|
|
3
|
+
|
|
4
|
+
A MessageCollectionRepository manages multiple collections,
|
|
5
|
+
providing CRUD operations over them.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
|
|
12
|
+
from switch_trust.eval.core.message.message_collection import MessageCollection
|
|
13
|
+
from switch_trust.eval.db.base.message.message_collection_types import (
|
|
14
|
+
DbMessageCollection,
|
|
15
|
+
MessageCollectionListView,
|
|
16
|
+
MessageCollectionSortOrder,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MessageCollectionRepository(ABC):
|
|
21
|
+
"""Repository that manages multiple MessageCollections."""
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def list(self) -> list[DbMessageCollection]:
|
|
25
|
+
"""Return all collections in the repository."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def search(
|
|
30
|
+
self,
|
|
31
|
+
query: str | None = None,
|
|
32
|
+
types: list[str] | None = None,
|
|
33
|
+
order: MessageCollectionSortOrder | None = None,
|
|
34
|
+
offset: int = 0,
|
|
35
|
+
limit: int = 50,
|
|
36
|
+
) -> MessageCollectionListView:
|
|
37
|
+
"""Search collections with pagination and sorting."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def get(self, id: str) -> DbMessageCollection:
|
|
42
|
+
"""Return the collection descriptor with the given id.
|
|
43
|
+
|
|
44
|
+
Raises KeyError if the collection does not exist.
|
|
45
|
+
"""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def get_message_collection(
|
|
50
|
+
self,
|
|
51
|
+
id: str,
|
|
52
|
+
) -> MessageCollection:
|
|
53
|
+
"""Return a MessageCollection instance for the given id.
|
|
54
|
+
|
|
55
|
+
Raises KeyError if the collection does not exist.
|
|
56
|
+
"""
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def create(
|
|
61
|
+
self,
|
|
62
|
+
db_collection: DbMessageCollection,
|
|
63
|
+
) -> DbMessageCollection:
|
|
64
|
+
"""Persist a new collection.
|
|
65
|
+
|
|
66
|
+
Raises ValueError if a collection with that name already
|
|
67
|
+
exists.
|
|
68
|
+
"""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def update(
|
|
73
|
+
self,
|
|
74
|
+
db_collection: DbMessageCollection,
|
|
75
|
+
) -> DbMessageCollection:
|
|
76
|
+
"""Replace an existing collection.
|
|
77
|
+
|
|
78
|
+
The collection is identified by ``db_collection.id``.
|
|
79
|
+
|
|
80
|
+
Raises KeyError if the collection does not exist.
|
|
81
|
+
"""
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
def delete(self, id: str) -> None:
|
|
86
|
+
"""Delete the collection with the given id.
|
|
87
|
+
|
|
88
|
+
Raises KeyError if the collection does not exist.
|
|
89
|
+
"""
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def clear(self) -> None:
|
|
94
|
+
"""Remove all collections from the repository."""
|
|
95
|
+
pass
|
|
@@ -0,0 +1,87 @@
|
|
|
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 MessageCollectionType(str, Enum):
|
|
13
|
+
"""Discriminator for the kind of MessageCollection."""
|
|
14
|
+
|
|
15
|
+
IN_MEMORY = "in-memory"
|
|
16
|
+
POSTGRES = "postgres"
|
|
17
|
+
GARAK = "garak"
|
|
18
|
+
CSV = "csv"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass_json
|
|
22
|
+
@dataclass
|
|
23
|
+
class DbMessageCollection:
|
|
24
|
+
"""JSON-serialisable description of a MessageCollection."""
|
|
25
|
+
|
|
26
|
+
type: MessageCollectionType
|
|
27
|
+
name: str
|
|
28
|
+
id: str = field(default_factory=generate_id)
|
|
29
|
+
created: datetime = field(
|
|
30
|
+
default_factory=now_utc,
|
|
31
|
+
metadata=datetime_config,
|
|
32
|
+
)
|
|
33
|
+
description: str | None = None
|
|
34
|
+
tags: dict[str, str] = field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
# -- in-memory type --
|
|
37
|
+
messages: list[dict] | None = None
|
|
38
|
+
prompts: list[str] | None = None
|
|
39
|
+
|
|
40
|
+
# -- garak type --
|
|
41
|
+
probe_name: str | None = None
|
|
42
|
+
|
|
43
|
+
# -- csv type --
|
|
44
|
+
filename: str | None = None
|
|
45
|
+
column: str | None = None
|
|
46
|
+
|
|
47
|
+
def get_ref(self) -> Reference:
|
|
48
|
+
return Reference(
|
|
49
|
+
self.id,
|
|
50
|
+
self.name,
|
|
51
|
+
ReferenceType.MESSAGE_COLLECTION,
|
|
52
|
+
self.description,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class MessageCollectionSortField(str, Enum):
|
|
57
|
+
NAME = "name"
|
|
58
|
+
CREATED = "created"
|
|
59
|
+
TYPE = "type"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass_json
|
|
63
|
+
@dataclass
|
|
64
|
+
class MessageCollectionSortOrder:
|
|
65
|
+
field: MessageCollectionSortField = MessageCollectionSortField.NAME
|
|
66
|
+
direction: SortDirection = SortDirection.ASC
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass_json
|
|
70
|
+
@dataclass
|
|
71
|
+
class MessageCollectionListView:
|
|
72
|
+
"""Paginated list of message collections."""
|
|
73
|
+
|
|
74
|
+
items: list[DbMessageCollection] = field(default_factory=list)
|
|
75
|
+
total: int = 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass_json
|
|
79
|
+
@dataclass
|
|
80
|
+
class MessageCollectionDetailView:
|
|
81
|
+
"""A message collection with its message count."""
|
|
82
|
+
|
|
83
|
+
id: str = ""
|
|
84
|
+
name: str = ""
|
|
85
|
+
description: str | None = None
|
|
86
|
+
tags: dict[str, str] = field(default_factory=dict)
|
|
87
|
+
message_count: int = 0
|
|
File without changes
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
|
|
5
|
+
from switch_trust.eval.common.utils import resolve_env, resolve_env_dict
|
|
6
|
+
from switch_trust.eval.core.models.model import Model
|
|
7
|
+
from switch_trust.eval.core.models.model_per_endpoint_limiter import (
|
|
8
|
+
PerEndpointLimiter,
|
|
9
|
+
)
|
|
10
|
+
from switch_trust.eval.core.models.model_retry import RetryModel
|
|
11
|
+
from switch_trust.eval.db.base.models.model_types import DbModel, ModelType
|
|
12
|
+
|
|
13
|
+
# HTTP-based agent types. SDK-based providers (Anthropic, OpenAI, Gemini,
|
|
14
|
+
# LiteLLM, HuggingFace, Ollama, OpenAI-compatible) own their own rate limiting
|
|
15
|
+
# and don't need our per-endpoint semaphore.
|
|
16
|
+
_LIMITED_HTTP_TYPES = {
|
|
17
|
+
ModelType.ADK,
|
|
18
|
+
ModelType.OPENAI_AGENT,
|
|
19
|
+
ModelType.ANTHROPIC_AGENT,
|
|
20
|
+
ModelType.GENERIC_HTTP,
|
|
21
|
+
ModelType.LANGSERVE,
|
|
22
|
+
ModelType.VERTEX_AGENT_RUNTIME,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_model(
|
|
27
|
+
db_model: DbModel,
|
|
28
|
+
max_retries: int = 5,
|
|
29
|
+
resolve_env_vars: bool = True,
|
|
30
|
+
connector_factory: Callable[[], aiohttp.BaseConnector] | None = None,
|
|
31
|
+
per_endpoint_limit: int = 8,
|
|
32
|
+
initial_jitter_max: float = 1.0,
|
|
33
|
+
) -> Model:
|
|
34
|
+
"""Create a Model instance from a DbModel, wrapped in a RetryModel for
|
|
35
|
+
transient error handling. HTTP-based types are additionally wrapped in a
|
|
36
|
+
PerEndpointLimiter so bursts against one tenant endpoint don't blow past
|
|
37
|
+
its QPS quota.
|
|
38
|
+
|
|
39
|
+
connector_factory, when set, is passed to the HTTP-based models so their
|
|
40
|
+
aiohttp sessions enforce the eval worker's request-time SSRF guard
|
|
41
|
+
(platform/ssrf.py). None keeps the default connector for CLI/local use.
|
|
42
|
+
"""
|
|
43
|
+
inner = _create_inner_model(
|
|
44
|
+
db_model,
|
|
45
|
+
resolve_env_vars=resolve_env_vars,
|
|
46
|
+
connector_factory=connector_factory,
|
|
47
|
+
)
|
|
48
|
+
if db_model.type in _LIMITED_HTTP_TYPES and db_model.host:
|
|
49
|
+
inner = PerEndpointLimiter(
|
|
50
|
+
inner,
|
|
51
|
+
endpoint_url=db_model.host,
|
|
52
|
+
limit=per_endpoint_limit,
|
|
53
|
+
jitter_max=initial_jitter_max,
|
|
54
|
+
)
|
|
55
|
+
return RetryModel(inner, max_retries=max_retries)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _create_inner_model(
|
|
59
|
+
db_model: DbModel,
|
|
60
|
+
resolve_env_vars: bool = True,
|
|
61
|
+
connector_factory: Callable[[], aiohttp.BaseConnector] | None = None,
|
|
62
|
+
) -> Model:
|
|
63
|
+
temp = db_model.temperature
|
|
64
|
+
if resolve_env_vars:
|
|
65
|
+
key = resolve_env(db_model.key)
|
|
66
|
+
headers = resolve_env_dict(db_model.headers)
|
|
67
|
+
else:
|
|
68
|
+
key = db_model.key
|
|
69
|
+
headers = dict(db_model.headers)
|
|
70
|
+
|
|
71
|
+
if db_model.type == ModelType.ANTHROPIC:
|
|
72
|
+
from anthropic import ( # noqa: PLC0415 - patched at source in tests
|
|
73
|
+
AsyncAnthropic,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
from switch_trust.eval.core.models.model_anthropic import ( # noqa: PLC0415 - patched at source in tests
|
|
77
|
+
AnthropicModel,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
client_kwargs = {}
|
|
81
|
+
if key:
|
|
82
|
+
client_kwargs["api_key"] = key
|
|
83
|
+
|
|
84
|
+
return AnthropicModel(
|
|
85
|
+
AsyncAnthropic(**client_kwargs),
|
|
86
|
+
db_model.model_name,
|
|
87
|
+
temperature=temp,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
elif db_model.type == ModelType.OPENAI:
|
|
91
|
+
from openai import AsyncOpenAI # noqa: PLC0415 - patched at source in tests
|
|
92
|
+
|
|
93
|
+
from switch_trust.eval.core.models.model_openai import ( # noqa: PLC0415 - patched at source in tests
|
|
94
|
+
OpenAIModel,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
client_kwargs = {}
|
|
98
|
+
if key:
|
|
99
|
+
client_kwargs["api_key"] = key
|
|
100
|
+
|
|
101
|
+
return OpenAIModel(
|
|
102
|
+
AsyncOpenAI(**client_kwargs),
|
|
103
|
+
db_model.model_name,
|
|
104
|
+
temperature=temp,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
elif db_model.type == ModelType.GEMINI:
|
|
108
|
+
from google.genai import Client # noqa: PLC0415 - patched at source in tests
|
|
109
|
+
|
|
110
|
+
from switch_trust.eval.core.models.model_gemini import ( # noqa: PLC0415 - patched at source in tests
|
|
111
|
+
GeminiModel,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
client_kwargs = {}
|
|
115
|
+
if key:
|
|
116
|
+
client_kwargs["api_key"] = key
|
|
117
|
+
|
|
118
|
+
return GeminiModel(
|
|
119
|
+
Client(**client_kwargs),
|
|
120
|
+
db_model.model_name,
|
|
121
|
+
temperature=temp,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
elif db_model.type == ModelType.LITELLM:
|
|
125
|
+
from switch_trust.eval.core.models.model_litellm import ( # noqa: PLC0415 - patched at source in tests
|
|
126
|
+
LiteLLMModel,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return LiteLLMModel(
|
|
130
|
+
db_model.model_name,
|
|
131
|
+
temperature=temp,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
elif db_model.type == ModelType.HUGGINGFACE:
|
|
135
|
+
from switch_trust.eval.core.models.model_huggingface import ( # noqa: PLC0415 - patched at source in tests
|
|
136
|
+
HuggingFaceModel,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return HuggingFaceModel(
|
|
140
|
+
db_model.model_name,
|
|
141
|
+
token=key or None,
|
|
142
|
+
temperature=temp,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
elif db_model.type == ModelType.OLLAMA:
|
|
146
|
+
from switch_trust.eval.core.models.model_ollama import ( # noqa: PLC0415 - patched at source in tests
|
|
147
|
+
OllamaModel,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return OllamaModel(
|
|
151
|
+
db_model.model_name,
|
|
152
|
+
host=db_model.host or "http://localhost:11434",
|
|
153
|
+
temperature=temp,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
elif db_model.type == ModelType.ADK:
|
|
157
|
+
from switch_trust.eval.core.models.model_adk import ( # noqa: PLC0415 - patched at source in tests
|
|
158
|
+
ADKModel,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return ADKModel(
|
|
162
|
+
app_name=db_model.model_name,
|
|
163
|
+
host=db_model.host or "http://localhost:8000",
|
|
164
|
+
immediate_result=db_model.immediate_result,
|
|
165
|
+
headers=headers or None,
|
|
166
|
+
connector_factory=connector_factory,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
elif db_model.type == ModelType.OPENAI_AGENT:
|
|
170
|
+
from switch_trust.eval.core.models.model_openai_agent import ( # noqa: PLC0415 - patched at source in tests
|
|
171
|
+
OpenAIAgentModel,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return OpenAIAgentModel(
|
|
175
|
+
host=db_model.host or "http://localhost:8000",
|
|
176
|
+
endpoint=db_model.endpoint or "/run",
|
|
177
|
+
connector_factory=connector_factory,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
elif db_model.type == ModelType.ANTHROPIC_AGENT:
|
|
181
|
+
from switch_trust.eval.core.models.model_anthropic_agent import ( # noqa: PLC0415 - patched at source in tests
|
|
182
|
+
AnthropicAgentModel,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return AnthropicAgentModel(
|
|
186
|
+
host=db_model.host or "http://localhost:8000",
|
|
187
|
+
endpoint=db_model.endpoint or "/run",
|
|
188
|
+
connector_factory=connector_factory,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
elif db_model.type == ModelType.OPENAI_COMPATIBLE:
|
|
192
|
+
from switch_trust.eval.core.models.model_openai_compatible import ( # noqa: PLC0415 - patched at source in tests
|
|
193
|
+
OpenAICompatibleModel,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return OpenAICompatibleModel(
|
|
197
|
+
base_url=db_model.host or "http://localhost:8000/v1",
|
|
198
|
+
model=db_model.model_name,
|
|
199
|
+
api_key=key or "EMPTY",
|
|
200
|
+
temperature=temp,
|
|
201
|
+
headers=headers or None,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
elif db_model.type == ModelType.GENERIC_HTTP:
|
|
205
|
+
from switch_trust.eval.core.models.model_generic_http import ( # noqa: PLC0415 - patched at source in tests
|
|
206
|
+
GenericHttpModel,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
url = db_model.host or "http://localhost:8000"
|
|
210
|
+
if db_model.endpoint:
|
|
211
|
+
url = f"{url.rstrip('/')}{db_model.endpoint}"
|
|
212
|
+
|
|
213
|
+
return GenericHttpModel(
|
|
214
|
+
url=url,
|
|
215
|
+
headers=headers or None,
|
|
216
|
+
input_path=db_model.input_path or "input",
|
|
217
|
+
output_path=db_model.output_path or "output",
|
|
218
|
+
connector_factory=connector_factory,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
elif db_model.type == ModelType.VERTEX_AGENT_RUNTIME:
|
|
222
|
+
from switch_trust.eval.core.models.model_vertex_agent_runtime import ( # noqa: PLC0415 - patched at source in tests
|
|
223
|
+
DEFAULT_CLASS_METHOD,
|
|
224
|
+
DEFAULT_USER_ID,
|
|
225
|
+
VertexAgentRuntimeModel,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# The engine URL already carries the :streamQuery suffix, so `endpoint`
|
|
229
|
+
# is not appended here the way it is for the generic HTTP types.
|
|
230
|
+
return VertexAgentRuntimeModel(
|
|
231
|
+
url=db_model.host or "",
|
|
232
|
+
credential=key or None,
|
|
233
|
+
headers=headers or None,
|
|
234
|
+
user_id=db_model.user_id or DEFAULT_USER_ID,
|
|
235
|
+
class_method=db_model.class_method or DEFAULT_CLASS_METHOD,
|
|
236
|
+
immediate_result=db_model.immediate_result,
|
|
237
|
+
connector_factory=connector_factory,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
elif db_model.type == ModelType.LANGSERVE:
|
|
241
|
+
from switch_trust.eval.core.models.model_langserve import ( # noqa: PLC0415 - patched at source in tests
|
|
242
|
+
LangServeModel,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return LangServeModel(
|
|
246
|
+
base_url=db_model.host or "http://localhost:8000",
|
|
247
|
+
chain_path=db_model.endpoint or "",
|
|
248
|
+
headers=headers or None,
|
|
249
|
+
connector_factory=connector_factory,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
else:
|
|
253
|
+
raise ValueError(f"unknown model type: {db_model.type}")
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Abstract interface for a repository of Models.
|
|
3
|
+
|
|
4
|
+
A ModelRepository manages model configurations,
|
|
5
|
+
providing CRUD operations over them. Models 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.models.model import Model
|
|
14
|
+
from switch_trust.eval.db.base.models.model_types import (
|
|
15
|
+
DbModel,
|
|
16
|
+
DbModelListView,
|
|
17
|
+
ModelSortOrder,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ModelRepository(ABC):
|
|
22
|
+
"""Repository that manages Model configurations."""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def list(self) -> list[DbModel]:
|
|
26
|
+
"""Return all model 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: ModelSortOrder | None = None,
|
|
35
|
+
offset: int = 0,
|
|
36
|
+
limit: int = 50,
|
|
37
|
+
) -> DbModelListView:
|
|
38
|
+
"""Search models by name with offset-based pagination.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
query: Text to match against the model name
|
|
42
|
+
(case-insensitive substring match).
|
|
43
|
+
order: Sort field and direction.
|
|
44
|
+
offset: Number of items to skip.
|
|
45
|
+
limit: Maximum number of items to return.
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def get(self, id: str) -> DbModel:
|
|
51
|
+
"""Return the model configuration with the given id.
|
|
52
|
+
|
|
53
|
+
Raises KeyError if the model does not exist.
|
|
54
|
+
"""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def create(
|
|
59
|
+
self,
|
|
60
|
+
config: DbModel,
|
|
61
|
+
) -> DbModel:
|
|
62
|
+
"""Persist a new model configuration.
|
|
63
|
+
|
|
64
|
+
Raises ValueError if a model with that name already
|
|
65
|
+
exists.
|
|
66
|
+
"""
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def update(
|
|
71
|
+
self,
|
|
72
|
+
config: DbModel,
|
|
73
|
+
) -> DbModel:
|
|
74
|
+
"""Replace an existing model configuration.
|
|
75
|
+
|
|
76
|
+
The model is identified by ``config.id``.
|
|
77
|
+
|
|
78
|
+
Raises KeyError if the model does not exist.
|
|
79
|
+
"""
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
def get_model(self, id: str) -> Model:
|
|
83
|
+
"""Return a Model instance for the given id.
|
|
84
|
+
|
|
85
|
+
Raises KeyError if the model does not exist.
|
|
86
|
+
"""
|
|
87
|
+
from switch_trust.eval.db.base.models.model_helpers import ( # noqa: PLC0415 - deferred import cost
|
|
88
|
+
create_model,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return create_model(self.get(id))
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
def delete(self, id: str) -> None:
|
|
95
|
+
"""Delete the model with the given id.
|
|
96
|
+
|
|
97
|
+
Raises KeyError if the model does not exist.
|
|
98
|
+
"""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def clear(self) -> None:
|
|
103
|
+
"""Remove all models from the repository."""
|
|
104
|
+
pass
|