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,159 @@
|
|
|
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
|
+
from switch_trust.eval.db.base.eval.model_eval_run_types import DbModelEvaluationRun
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ModelType(str, Enum):
|
|
14
|
+
"""Discriminator for the kind of Model."""
|
|
15
|
+
|
|
16
|
+
ANTHROPIC = "anthropic"
|
|
17
|
+
OPENAI = "openai"
|
|
18
|
+
GEMINI = "gemini"
|
|
19
|
+
LITELLM = "litellm"
|
|
20
|
+
HUGGINGFACE = "huggingface"
|
|
21
|
+
OLLAMA = "ollama"
|
|
22
|
+
ADK = "adk"
|
|
23
|
+
OPENAI_AGENT = "openai_agent"
|
|
24
|
+
ANTHROPIC_AGENT = "anthropic_agent"
|
|
25
|
+
OPENAI_COMPATIBLE = "openai_compatible"
|
|
26
|
+
GENERIC_HTTP = "generic_http"
|
|
27
|
+
LANGSERVE = "langserve"
|
|
28
|
+
VERTEX_AGENT_RUNTIME = "vertex_agent_runtime"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass_json
|
|
32
|
+
@dataclass
|
|
33
|
+
class DbModel:
|
|
34
|
+
"""JSON-serialisable description of a Model."""
|
|
35
|
+
|
|
36
|
+
type: ModelType
|
|
37
|
+
name: str
|
|
38
|
+
model_name: str
|
|
39
|
+
id: str = field(default_factory=generate_id)
|
|
40
|
+
created: datetime = field(
|
|
41
|
+
default_factory=now_utc,
|
|
42
|
+
metadata=datetime_config,
|
|
43
|
+
)
|
|
44
|
+
description: str | None = None
|
|
45
|
+
|
|
46
|
+
tags: dict[str, str] = field(default_factory=dict)
|
|
47
|
+
|
|
48
|
+
# -- common optional --
|
|
49
|
+
key: str | None = None
|
|
50
|
+
temperature: float = 0.0
|
|
51
|
+
|
|
52
|
+
# -- ollama / agent types --
|
|
53
|
+
host: str | None = None
|
|
54
|
+
endpoint: str | None = None
|
|
55
|
+
|
|
56
|
+
# -- adk / vertex_agent_runtime --
|
|
57
|
+
immediate_result: bool = False
|
|
58
|
+
|
|
59
|
+
# -- vertex_agent_runtime --
|
|
60
|
+
user_id: str | None = None
|
|
61
|
+
class_method: str | None = None
|
|
62
|
+
|
|
63
|
+
# -- generic_http / openai_compatible / langserve --
|
|
64
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
65
|
+
input_path: str | None = None
|
|
66
|
+
output_path: str | None = None
|
|
67
|
+
|
|
68
|
+
def get_ref(self) -> Reference:
|
|
69
|
+
return Reference(
|
|
70
|
+
self.id,
|
|
71
|
+
self.name,
|
|
72
|
+
ReferenceType.MODEL,
|
|
73
|
+
self.description,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ModelSortField(str, Enum):
|
|
78
|
+
NAME = "name"
|
|
79
|
+
CREATED = "created"
|
|
80
|
+
TYPE = "type"
|
|
81
|
+
SCORE = "score"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass_json
|
|
85
|
+
@dataclass
|
|
86
|
+
class ModelSortOrder:
|
|
87
|
+
field: ModelSortField = ModelSortField.NAME
|
|
88
|
+
direction: SortDirection = SortDirection.ASC
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass_json
|
|
92
|
+
@dataclass
|
|
93
|
+
class ModelScoreSummary:
|
|
94
|
+
"""Aggregate score and run status for a model."""
|
|
95
|
+
|
|
96
|
+
overall_score: float | None = None
|
|
97
|
+
finished_evaluations: int = 0
|
|
98
|
+
running_evaluations: int = 0
|
|
99
|
+
errored_evaluations: int = 0
|
|
100
|
+
pending_evaluations: int = 0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass_json
|
|
104
|
+
@dataclass
|
|
105
|
+
class ModelListViewItem:
|
|
106
|
+
"""A model with its aggregate score summary."""
|
|
107
|
+
|
|
108
|
+
model: DbModel | None = None
|
|
109
|
+
score_summary: ModelScoreSummary = field(
|
|
110
|
+
default_factory=ModelScoreSummary,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass_json
|
|
115
|
+
@dataclass
|
|
116
|
+
class ModelListView:
|
|
117
|
+
"""Paginated list of model configurations with scores."""
|
|
118
|
+
|
|
119
|
+
items: list[ModelListViewItem] = field(
|
|
120
|
+
default_factory=list,
|
|
121
|
+
)
|
|
122
|
+
total: int = 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass_json
|
|
126
|
+
@dataclass
|
|
127
|
+
class DbModelListView:
|
|
128
|
+
"""Raw paginated list from the repository."""
|
|
129
|
+
|
|
130
|
+
items: list[DbModel] = field(default_factory=list)
|
|
131
|
+
total: int = 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass_json
|
|
135
|
+
@dataclass
|
|
136
|
+
class ModelDetailEvaluationView:
|
|
137
|
+
"""A single ModelEvaluation with references and last run."""
|
|
138
|
+
|
|
139
|
+
id: str
|
|
140
|
+
model_ref: Reference
|
|
141
|
+
evaluation_ref: Reference
|
|
142
|
+
created: datetime = field(metadata=datetime_config)
|
|
143
|
+
weight: float = 0.5
|
|
144
|
+
evaluation_approach: str = "Probe"
|
|
145
|
+
evaluation_tags: dict[str, str] = field(
|
|
146
|
+
default_factory=dict,
|
|
147
|
+
)
|
|
148
|
+
last_run: DbModelEvaluationRun | None = None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass_json
|
|
152
|
+
@dataclass
|
|
153
|
+
class ModelDetailView:
|
|
154
|
+
"""A model configuration with its evaluations."""
|
|
155
|
+
|
|
156
|
+
model: DbModel | None = None
|
|
157
|
+
evaluations: list[ModelDetailEvaluationView] = field(
|
|
158
|
+
default_factory=list,
|
|
159
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Read-only JSON-backed repositories for CLI evaluation runs.
|
|
3
|
+
|
|
4
|
+
Reads a single JSON configuration file and provides repository
|
|
5
|
+
implementations for models, evaluations, detectors, message
|
|
6
|
+
collections, and model evaluations. All write operations raise
|
|
7
|
+
NotImplementedError.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
from switch_trust.eval.common.reference import Reference
|
|
16
|
+
from switch_trust.eval.common.utils import strip_nulls
|
|
17
|
+
from switch_trust.eval.core.message.message_collection import MessageCollection
|
|
18
|
+
from switch_trust.eval.db.base.detectors.detector_repository import DetectorRepository
|
|
19
|
+
from switch_trust.eval.db.base.detectors.detector_types import (
|
|
20
|
+
DbDetector,
|
|
21
|
+
DetectorListView,
|
|
22
|
+
DetectorSortOrder,
|
|
23
|
+
)
|
|
24
|
+
from switch_trust.eval.db.base.eval.eval_repository import EvaluationRepository
|
|
25
|
+
from switch_trust.eval.db.base.eval.eval_types import (
|
|
26
|
+
DbEvaluation,
|
|
27
|
+
EvaluationListView,
|
|
28
|
+
EvaluationSortOrder,
|
|
29
|
+
)
|
|
30
|
+
from switch_trust.eval.db.base.eval.model_eval_repository import (
|
|
31
|
+
ModelEvaluationRepository,
|
|
32
|
+
)
|
|
33
|
+
from switch_trust.eval.db.base.eval.model_eval_types import (
|
|
34
|
+
DbModelEvaluation,
|
|
35
|
+
ModelEvaluationListItem,
|
|
36
|
+
ModelEvaluationListView,
|
|
37
|
+
ModelEvaluationSortOrder,
|
|
38
|
+
)
|
|
39
|
+
from switch_trust.eval.db.base.message.message_collection_helpers import (
|
|
40
|
+
create_message_collection,
|
|
41
|
+
)
|
|
42
|
+
from switch_trust.eval.db.base.message.message_collection_repository import (
|
|
43
|
+
MessageCollectionRepository,
|
|
44
|
+
)
|
|
45
|
+
from switch_trust.eval.db.base.message.message_collection_types import (
|
|
46
|
+
DbMessageCollection,
|
|
47
|
+
MessageCollectionListView,
|
|
48
|
+
MessageCollectionSortOrder,
|
|
49
|
+
MessageCollectionType,
|
|
50
|
+
)
|
|
51
|
+
from switch_trust.eval.db.base.models.model_repository import ModelRepository
|
|
52
|
+
from switch_trust.eval.db.base.models.model_types import (
|
|
53
|
+
DbModel,
|
|
54
|
+
DbModelListView,
|
|
55
|
+
ModelSortOrder,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
_READ_ONLY = NotImplementedError("JSON store is read-only")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _filter_by_query(
|
|
62
|
+
items: list,
|
|
63
|
+
query: str | None,
|
|
64
|
+
name_attr: str = "name",
|
|
65
|
+
) -> list:
|
|
66
|
+
if not query:
|
|
67
|
+
return items
|
|
68
|
+
q = query.lower()
|
|
69
|
+
return [item for item in items if q in getattr(item, name_attr).lower()]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class JsonModelRepository(ModelRepository):
|
|
73
|
+
def __init__(self, models: list[DbModel]):
|
|
74
|
+
self._models = models
|
|
75
|
+
self._by_id = {m.id: m for m in models}
|
|
76
|
+
|
|
77
|
+
def list(self) -> list[DbModel]:
|
|
78
|
+
return list(self._models)
|
|
79
|
+
|
|
80
|
+
def search(
|
|
81
|
+
self,
|
|
82
|
+
query: str | None = None,
|
|
83
|
+
types: list[str] | None = None,
|
|
84
|
+
order: ModelSortOrder | None = None,
|
|
85
|
+
offset: int = 0,
|
|
86
|
+
limit: int = 50,
|
|
87
|
+
) -> DbModelListView:
|
|
88
|
+
items = _filter_by_query(self._models, query)
|
|
89
|
+
if types:
|
|
90
|
+
items = [m for m in items if m.type.value in types]
|
|
91
|
+
total = len(items)
|
|
92
|
+
items = items[offset : offset + limit]
|
|
93
|
+
return DbModelListView(items=items, total=total)
|
|
94
|
+
|
|
95
|
+
def get(self, id: str) -> DbModel:
|
|
96
|
+
if id not in self._by_id:
|
|
97
|
+
raise KeyError(f"Model {id!r} not found")
|
|
98
|
+
return self._by_id[id]
|
|
99
|
+
|
|
100
|
+
def create(self, config: DbModel) -> DbModel:
|
|
101
|
+
raise _READ_ONLY
|
|
102
|
+
|
|
103
|
+
def update(self, config: DbModel) -> DbModel:
|
|
104
|
+
raise _READ_ONLY
|
|
105
|
+
|
|
106
|
+
def delete(self, id: str) -> None:
|
|
107
|
+
raise _READ_ONLY
|
|
108
|
+
|
|
109
|
+
def clear(self) -> None:
|
|
110
|
+
raise _READ_ONLY
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class JsonEvaluationRepository(EvaluationRepository):
|
|
114
|
+
def __init__(self, evaluations: list[DbEvaluation]):
|
|
115
|
+
self._evaluations = evaluations
|
|
116
|
+
self._by_id = {e.id: e for e in evaluations}
|
|
117
|
+
|
|
118
|
+
def list(self) -> list[DbEvaluation]:
|
|
119
|
+
return list(self._evaluations)
|
|
120
|
+
|
|
121
|
+
def search(
|
|
122
|
+
self,
|
|
123
|
+
query: str | None = None,
|
|
124
|
+
types: list[str] | None = None,
|
|
125
|
+
order: EvaluationSortOrder | None = None,
|
|
126
|
+
offset: int = 0,
|
|
127
|
+
limit: int = 50,
|
|
128
|
+
) -> EvaluationListView:
|
|
129
|
+
items = _filter_by_query(self._evaluations, query)
|
|
130
|
+
if types:
|
|
131
|
+
items = [e for e in items if e.type.value in types]
|
|
132
|
+
total = len(items)
|
|
133
|
+
items = items[offset : offset + limit]
|
|
134
|
+
return EvaluationListView(items=items, total=total)
|
|
135
|
+
|
|
136
|
+
def get(self, id: str) -> DbEvaluation:
|
|
137
|
+
if id not in self._by_id:
|
|
138
|
+
raise KeyError(
|
|
139
|
+
f"Evaluation {id!r} not found",
|
|
140
|
+
)
|
|
141
|
+
return self._by_id[id]
|
|
142
|
+
|
|
143
|
+
def create(self, config: DbEvaluation) -> DbEvaluation:
|
|
144
|
+
raise _READ_ONLY
|
|
145
|
+
|
|
146
|
+
def update(self, config: DbEvaluation) -> DbEvaluation:
|
|
147
|
+
raise _READ_ONLY
|
|
148
|
+
|
|
149
|
+
def delete(self, id: str) -> None:
|
|
150
|
+
raise _READ_ONLY
|
|
151
|
+
|
|
152
|
+
def clear(self) -> None:
|
|
153
|
+
raise _READ_ONLY
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class JsonDetectorRepository(DetectorRepository):
|
|
157
|
+
def __init__(self, detectors: list[DbDetector]):
|
|
158
|
+
self._detectors = detectors
|
|
159
|
+
self._by_id = {d.id: d for d in detectors}
|
|
160
|
+
|
|
161
|
+
def list(self) -> list[DbDetector]:
|
|
162
|
+
return list(self._detectors)
|
|
163
|
+
|
|
164
|
+
def search(
|
|
165
|
+
self,
|
|
166
|
+
query: str | None = None,
|
|
167
|
+
types: list[str] | None = None,
|
|
168
|
+
order: DetectorSortOrder | None = None,
|
|
169
|
+
offset: int = 0,
|
|
170
|
+
limit: int = 50,
|
|
171
|
+
) -> DetectorListView:
|
|
172
|
+
items = _filter_by_query(self._detectors, query)
|
|
173
|
+
if types:
|
|
174
|
+
items = [d for d in items if d.type.value in types]
|
|
175
|
+
total = len(items)
|
|
176
|
+
items = items[offset : offset + limit]
|
|
177
|
+
return DetectorListView(items=items, total=total)
|
|
178
|
+
|
|
179
|
+
def get(self, id: str) -> DbDetector:
|
|
180
|
+
if id not in self._by_id:
|
|
181
|
+
raise KeyError(
|
|
182
|
+
f"Detector {id!r} not found",
|
|
183
|
+
)
|
|
184
|
+
return self._by_id[id]
|
|
185
|
+
|
|
186
|
+
def create(self, config: DbDetector) -> DbDetector:
|
|
187
|
+
raise _READ_ONLY
|
|
188
|
+
|
|
189
|
+
def update(self, config: DbDetector) -> DbDetector:
|
|
190
|
+
raise _READ_ONLY
|
|
191
|
+
|
|
192
|
+
def delete(self, id: str) -> None:
|
|
193
|
+
raise _READ_ONLY
|
|
194
|
+
|
|
195
|
+
def clear(self) -> None:
|
|
196
|
+
raise _READ_ONLY
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class JsonMessageCollectionRepository(
|
|
200
|
+
MessageCollectionRepository,
|
|
201
|
+
):
|
|
202
|
+
def __init__(
|
|
203
|
+
self,
|
|
204
|
+
collections: list[DbMessageCollection],
|
|
205
|
+
):
|
|
206
|
+
self._collections = collections
|
|
207
|
+
self._by_id = {c.id: c for c in collections}
|
|
208
|
+
|
|
209
|
+
def list(self) -> list[DbMessageCollection]:
|
|
210
|
+
return list(self._collections)
|
|
211
|
+
|
|
212
|
+
def search(
|
|
213
|
+
self,
|
|
214
|
+
query: str | None = None,
|
|
215
|
+
types: list[str] | None = None,
|
|
216
|
+
order: MessageCollectionSortOrder | None = None,
|
|
217
|
+
offset: int = 0,
|
|
218
|
+
limit: int = 50,
|
|
219
|
+
) -> MessageCollectionListView:
|
|
220
|
+
items = _filter_by_query(
|
|
221
|
+
self._collections,
|
|
222
|
+
query,
|
|
223
|
+
)
|
|
224
|
+
if types:
|
|
225
|
+
items = [c for c in items if c.type.value in types]
|
|
226
|
+
total = len(items)
|
|
227
|
+
items = items[offset : offset + limit]
|
|
228
|
+
return MessageCollectionListView(
|
|
229
|
+
items=items,
|
|
230
|
+
total=total,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def get(self, id: str) -> DbMessageCollection:
|
|
234
|
+
if id not in self._by_id:
|
|
235
|
+
raise KeyError(
|
|
236
|
+
f"MessageCollection {id!r} not found",
|
|
237
|
+
)
|
|
238
|
+
return self._by_id[id]
|
|
239
|
+
|
|
240
|
+
def get_message_collection(
|
|
241
|
+
self,
|
|
242
|
+
id: str,
|
|
243
|
+
) -> MessageCollection:
|
|
244
|
+
|
|
245
|
+
return create_message_collection(self.get(id))
|
|
246
|
+
|
|
247
|
+
def create(
|
|
248
|
+
self,
|
|
249
|
+
db_collection: DbMessageCollection,
|
|
250
|
+
) -> DbMessageCollection:
|
|
251
|
+
raise _READ_ONLY
|
|
252
|
+
|
|
253
|
+
def update(
|
|
254
|
+
self,
|
|
255
|
+
db_collection: DbMessageCollection,
|
|
256
|
+
) -> DbMessageCollection:
|
|
257
|
+
raise _READ_ONLY
|
|
258
|
+
|
|
259
|
+
def delete(self, id: str) -> None:
|
|
260
|
+
raise _READ_ONLY
|
|
261
|
+
|
|
262
|
+
def clear(self) -> None:
|
|
263
|
+
raise _READ_ONLY
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class JsonModelEvaluationRepository(
|
|
267
|
+
ModelEvaluationRepository,
|
|
268
|
+
):
|
|
269
|
+
def __init__(
|
|
270
|
+
self,
|
|
271
|
+
model_evaluations: list[DbModelEvaluation],
|
|
272
|
+
model_repo: JsonModelRepository,
|
|
273
|
+
eval_repo: JsonEvaluationRepository,
|
|
274
|
+
):
|
|
275
|
+
self._items = model_evaluations
|
|
276
|
+
self._by_id = {me.id: me for me in model_evaluations}
|
|
277
|
+
self._model_repo = model_repo
|
|
278
|
+
self._eval_repo = eval_repo
|
|
279
|
+
|
|
280
|
+
def _resolve_refs(
|
|
281
|
+
self,
|
|
282
|
+
me: DbModelEvaluation,
|
|
283
|
+
) -> ModelEvaluationListItem:
|
|
284
|
+
model_ref: Reference | None = None
|
|
285
|
+
eval_ref: Reference | None = None
|
|
286
|
+
try:
|
|
287
|
+
model_ref = self._model_repo.get(
|
|
288
|
+
me.model_id,
|
|
289
|
+
).get_ref()
|
|
290
|
+
except KeyError:
|
|
291
|
+
pass
|
|
292
|
+
try:
|
|
293
|
+
eval_ref = self._eval_repo.get(
|
|
294
|
+
me.evaluation_id,
|
|
295
|
+
).get_ref()
|
|
296
|
+
except KeyError:
|
|
297
|
+
pass
|
|
298
|
+
return ModelEvaluationListItem(
|
|
299
|
+
config=me,
|
|
300
|
+
model_ref=model_ref,
|
|
301
|
+
evaluation_ref=eval_ref,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def list_by_model(
|
|
305
|
+
self,
|
|
306
|
+
model_id: str,
|
|
307
|
+
order: ModelEvaluationSortOrder | None = None,
|
|
308
|
+
offset: int = 0,
|
|
309
|
+
limit: int = 50,
|
|
310
|
+
) -> ModelEvaluationListView:
|
|
311
|
+
filtered = [me for me in self._items if me.model_id == model_id]
|
|
312
|
+
total = len(filtered)
|
|
313
|
+
page = filtered[offset : offset + limit]
|
|
314
|
+
items = [self._resolve_refs(me) for me in page]
|
|
315
|
+
return ModelEvaluationListView(
|
|
316
|
+
items=items,
|
|
317
|
+
total=total,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
def list_by_evaluation(
|
|
321
|
+
self,
|
|
322
|
+
evaluation_id: str,
|
|
323
|
+
order: ModelEvaluationSortOrder | None = None,
|
|
324
|
+
offset: int = 0,
|
|
325
|
+
limit: int = 50,
|
|
326
|
+
) -> ModelEvaluationListView:
|
|
327
|
+
filtered = [me for me in self._items if me.evaluation_id == evaluation_id]
|
|
328
|
+
total = len(filtered)
|
|
329
|
+
page = filtered[offset : offset + limit]
|
|
330
|
+
items = [self._resolve_refs(me) for me in page]
|
|
331
|
+
return ModelEvaluationListView(
|
|
332
|
+
items=items,
|
|
333
|
+
total=total,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def get(self, id: str) -> DbModelEvaluation:
|
|
337
|
+
if id not in self._by_id:
|
|
338
|
+
raise KeyError(
|
|
339
|
+
f"ModelEvaluation {id!r} not found",
|
|
340
|
+
)
|
|
341
|
+
return self._by_id[id]
|
|
342
|
+
|
|
343
|
+
def list_all(self) -> list[DbModelEvaluation]:
|
|
344
|
+
return list(self._items)
|
|
345
|
+
|
|
346
|
+
def add(self, config: DbModelEvaluation) -> bool:
|
|
347
|
+
"""Add a model-evaluation assignment.
|
|
348
|
+
|
|
349
|
+
Returns True if added, False if a matching
|
|
350
|
+
model_id + evaluation_id pair already exists.
|
|
351
|
+
"""
|
|
352
|
+
for existing in self._items:
|
|
353
|
+
if (
|
|
354
|
+
existing.model_id == config.model_id
|
|
355
|
+
and existing.evaluation_id == config.evaluation_id
|
|
356
|
+
):
|
|
357
|
+
return False
|
|
358
|
+
self._items.append(config)
|
|
359
|
+
self._by_id[config.id] = config
|
|
360
|
+
return True
|
|
361
|
+
|
|
362
|
+
def remove(
|
|
363
|
+
self,
|
|
364
|
+
model_id: str | None = None,
|
|
365
|
+
evaluation_id: str | None = None,
|
|
366
|
+
) -> list[DbModelEvaluation]:
|
|
367
|
+
"""Remove assignments matching the given filters.
|
|
368
|
+
|
|
369
|
+
At least one of model_id or evaluation_id must be
|
|
370
|
+
provided. Returns the list of removed items.
|
|
371
|
+
"""
|
|
372
|
+
if model_id is None and evaluation_id is None:
|
|
373
|
+
return []
|
|
374
|
+
|
|
375
|
+
removed: list[DbModelEvaluation] = []
|
|
376
|
+
kept: list[DbModelEvaluation] = []
|
|
377
|
+
for me in self._items:
|
|
378
|
+
match = True
|
|
379
|
+
if model_id is not None and me.model_id != model_id:
|
|
380
|
+
match = False
|
|
381
|
+
if evaluation_id is not None and me.evaluation_id != evaluation_id:
|
|
382
|
+
match = False
|
|
383
|
+
if match:
|
|
384
|
+
removed.append(me)
|
|
385
|
+
else:
|
|
386
|
+
kept.append(me)
|
|
387
|
+
|
|
388
|
+
self._items = kept
|
|
389
|
+
self._by_id = {me.id: me for me in kept}
|
|
390
|
+
return removed
|
|
391
|
+
|
|
392
|
+
def create(
|
|
393
|
+
self,
|
|
394
|
+
config: DbModelEvaluation,
|
|
395
|
+
) -> DbModelEvaluation:
|
|
396
|
+
raise _READ_ONLY
|
|
397
|
+
|
|
398
|
+
def clear(self) -> None:
|
|
399
|
+
raise _READ_ONLY
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class JsonRepository:
|
|
403
|
+
"""Read-only store backed by a single JSON config file."""
|
|
404
|
+
|
|
405
|
+
def __init__(self, path: str):
|
|
406
|
+
self._path = path
|
|
407
|
+
with open(path) as f:
|
|
408
|
+
data = json.load(f)
|
|
409
|
+
self._init_repos(data)
|
|
410
|
+
|
|
411
|
+
def merge(self, other: JsonRepository) -> JsonRepository:
|
|
412
|
+
"""Return a new repository with entries from both.
|
|
413
|
+
|
|
414
|
+
On ID conflicts, entries from ``self`` take precedence.
|
|
415
|
+
"""
|
|
416
|
+
|
|
417
|
+
def _combine(self_list: list, other_list: list) -> list:
|
|
418
|
+
self_ids = {item.id for item in self_list}
|
|
419
|
+
combined = list(self_list)
|
|
420
|
+
for item in other_list:
|
|
421
|
+
if item.id not in self_ids:
|
|
422
|
+
combined.append(item)
|
|
423
|
+
return combined
|
|
424
|
+
|
|
425
|
+
merged = object.__new__(JsonRepository)
|
|
426
|
+
merged._path = self._path
|
|
427
|
+
|
|
428
|
+
models = _combine(
|
|
429
|
+
self._models_repo.list(),
|
|
430
|
+
other._models_repo.list(),
|
|
431
|
+
)
|
|
432
|
+
evals = _combine(
|
|
433
|
+
self._evaluations_repo.list(),
|
|
434
|
+
other._evaluations_repo.list(),
|
|
435
|
+
)
|
|
436
|
+
detectors = _combine(
|
|
437
|
+
self._detectors_repo.list(),
|
|
438
|
+
other._detectors_repo.list(),
|
|
439
|
+
)
|
|
440
|
+
collections = _combine(
|
|
441
|
+
self._message_collections_repo.list(),
|
|
442
|
+
other._message_collections_repo.list(),
|
|
443
|
+
)
|
|
444
|
+
assignments = _combine(
|
|
445
|
+
self._model_evaluations_repo.list_all(),
|
|
446
|
+
other._model_evaluations_repo.list_all(),
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
merged._models_repo = JsonModelRepository(models)
|
|
450
|
+
merged._evaluations_repo = JsonEvaluationRepository(evals)
|
|
451
|
+
merged._detectors_repo = JsonDetectorRepository(detectors)
|
|
452
|
+
merged._message_collections_repo = JsonMessageCollectionRepository(collections)
|
|
453
|
+
merged._model_evaluations_repo = JsonModelEvaluationRepository(
|
|
454
|
+
assignments,
|
|
455
|
+
model_repo=merged._models_repo,
|
|
456
|
+
eval_repo=merged._evaluations_repo,
|
|
457
|
+
)
|
|
458
|
+
return merged
|
|
459
|
+
|
|
460
|
+
def _init_repos(self, data: dict) -> None:
|
|
461
|
+
config_dir = os.path.dirname(
|
|
462
|
+
os.path.abspath(self._path),
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
self._models_repo = JsonModelRepository(
|
|
466
|
+
[DbModel.from_dict(m) for m in data.get("models", [])]
|
|
467
|
+
)
|
|
468
|
+
self._evaluations_repo = JsonEvaluationRepository(
|
|
469
|
+
[DbEvaluation.from_dict(e) for e in data.get("evaluations", [])]
|
|
470
|
+
)
|
|
471
|
+
detectors: list[DbDetector] = []
|
|
472
|
+
for d_dict in data.get("detectors", []):
|
|
473
|
+
prompt_file = d_dict.pop("prompt_file", None)
|
|
474
|
+
if prompt_file:
|
|
475
|
+
if not os.path.isabs(prompt_file):
|
|
476
|
+
prompt_file = os.path.join(
|
|
477
|
+
config_dir,
|
|
478
|
+
prompt_file,
|
|
479
|
+
)
|
|
480
|
+
with open(
|
|
481
|
+
prompt_file,
|
|
482
|
+
encoding="utf-8",
|
|
483
|
+
) as f:
|
|
484
|
+
d_dict["prompt"] = f.read()
|
|
485
|
+
d = DbDetector.from_dict(d_dict)
|
|
486
|
+
detectors.append(d)
|
|
487
|
+
self._detectors_repo = JsonDetectorRepository(detectors)
|
|
488
|
+
|
|
489
|
+
collections: list[DbMessageCollection] = []
|
|
490
|
+
for mc_dict in data.get("message_collections", []):
|
|
491
|
+
mc = DbMessageCollection.from_dict(mc_dict)
|
|
492
|
+
if (
|
|
493
|
+
mc.type == MessageCollectionType.CSV
|
|
494
|
+
and mc.filename
|
|
495
|
+
and not os.path.isabs(mc.filename)
|
|
496
|
+
):
|
|
497
|
+
mc.filename = os.path.join(
|
|
498
|
+
config_dir,
|
|
499
|
+
mc.filename,
|
|
500
|
+
)
|
|
501
|
+
collections.append(mc)
|
|
502
|
+
|
|
503
|
+
self._message_collections_repo = JsonMessageCollectionRepository(collections)
|
|
504
|
+
self._model_evaluations_repo = JsonModelEvaluationRepository(
|
|
505
|
+
[
|
|
506
|
+
DbModelEvaluation.from_dict(me)
|
|
507
|
+
for me in data.get(
|
|
508
|
+
"model_evaluations",
|
|
509
|
+
[],
|
|
510
|
+
)
|
|
511
|
+
],
|
|
512
|
+
model_repo=self._models_repo,
|
|
513
|
+
eval_repo=self._evaluations_repo,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
def save(self) -> None:
|
|
517
|
+
"""Write the current state back to the config file."""
|
|
518
|
+
data: dict = {
|
|
519
|
+
"models": [m.to_dict() for m in self._models_repo.list()],
|
|
520
|
+
"evaluations": [e.to_dict() for e in self._evaluations_repo.list()],
|
|
521
|
+
"detectors": [d.to_dict() for d in self._detectors_repo.list()],
|
|
522
|
+
"message_collections": [
|
|
523
|
+
c.to_dict() for c in self._message_collections_repo.list()
|
|
524
|
+
],
|
|
525
|
+
"model_evaluations": [
|
|
526
|
+
me.to_dict() for me in self._model_evaluations_repo.list_all()
|
|
527
|
+
],
|
|
528
|
+
}
|
|
529
|
+
data = strip_nulls(data)
|
|
530
|
+
with open(self._path, "w") as f:
|
|
531
|
+
json.dump(data, f, indent=2, default=str)
|
|
532
|
+
|
|
533
|
+
@property
|
|
534
|
+
def path(self) -> str:
|
|
535
|
+
return self._path
|
|
536
|
+
|
|
537
|
+
@property
|
|
538
|
+
def models(self) -> JsonModelRepository:
|
|
539
|
+
return self._models_repo
|
|
540
|
+
|
|
541
|
+
@property
|
|
542
|
+
def evaluations(self) -> JsonEvaluationRepository:
|
|
543
|
+
return self._evaluations_repo
|
|
544
|
+
|
|
545
|
+
@property
|
|
546
|
+
def detectors(self) -> JsonDetectorRepository:
|
|
547
|
+
return self._detectors_repo
|
|
548
|
+
|
|
549
|
+
@property
|
|
550
|
+
def message_collections(
|
|
551
|
+
self,
|
|
552
|
+
) -> JsonMessageCollectionRepository:
|
|
553
|
+
return self._message_collections_repo
|
|
554
|
+
|
|
555
|
+
@property
|
|
556
|
+
def model_evaluations(
|
|
557
|
+
self,
|
|
558
|
+
) -> JsonModelEvaluationRepository:
|
|
559
|
+
return self._model_evaluations_repo
|