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
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Convert between Anthropic Messages API types and internal schema."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from anthropic.types import Message as AnthropicMessage
|
|
8
|
+
from anthropic.types import MessageParam, TextBlock, ThinkingBlock, ToolUseBlock
|
|
9
|
+
|
|
10
|
+
from switch_trust.eval.common.schema import (
|
|
11
|
+
Content,
|
|
12
|
+
Message,
|
|
13
|
+
Part,
|
|
14
|
+
PartType,
|
|
15
|
+
Role,
|
|
16
|
+
ToolCall,
|
|
17
|
+
ToolResult,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# -- To internal --------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def to_content(message: dict[str, Any] | AnthropicMessage) -> Content:
|
|
24
|
+
"""Convert an Anthropic message (param dict or response object) to Content."""
|
|
25
|
+
if isinstance(message, AnthropicMessage):
|
|
26
|
+
return _response_to_content(message)
|
|
27
|
+
return _param_to_content(message)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def to_message(message: dict[str, Any] | AnthropicMessage) -> Message:
|
|
31
|
+
return Message(content=to_content(message))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# -- From internal ------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def from_content(content: Content) -> dict[str, Any]:
|
|
38
|
+
"""Convert internal Content to an Anthropic MessageParam dict.
|
|
39
|
+
|
|
40
|
+
System content is returned as ``{"role": "system", "content": ...}``
|
|
41
|
+
— the caller should extract system messages and pass them via the
|
|
42
|
+
``system`` parameter of ``messages.create()``.
|
|
43
|
+
"""
|
|
44
|
+
role = content.role.value
|
|
45
|
+
|
|
46
|
+
if role == "system":
|
|
47
|
+
return {"role": "system", "content": _parts_to_blocks(content.parts)}
|
|
48
|
+
|
|
49
|
+
return {"role": role, "content": _parts_to_blocks(content.parts)}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def from_message(message: Message) -> dict[str, Any]:
|
|
53
|
+
return from_content(message.content)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def from_content_obj(content: Content) -> MessageParam:
|
|
57
|
+
"""Convert internal Content to a typed Anthropic MessageParam."""
|
|
58
|
+
return from_content(content) # type: ignore[return-value]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def from_message_obj(message: Message) -> MessageParam:
|
|
62
|
+
return from_content_obj(message.content)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# -- Private: to internal -----------------------------------------------------
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _response_to_content(msg: AnthropicMessage) -> Content:
|
|
69
|
+
parts: list[Part] = []
|
|
70
|
+
for block in msg.content:
|
|
71
|
+
if isinstance(block, TextBlock):
|
|
72
|
+
parts.append(Part.text_part(block.text))
|
|
73
|
+
elif isinstance(block, ToolUseBlock):
|
|
74
|
+
parts.append(
|
|
75
|
+
Part.tool_call_part(
|
|
76
|
+
ToolCall(
|
|
77
|
+
id=block.id,
|
|
78
|
+
name=block.name,
|
|
79
|
+
arguments=block.input if isinstance(block.input, dict) else {},
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
elif isinstance(block, ThinkingBlock):
|
|
84
|
+
parts.append(Part.thinking_part(block.thinking))
|
|
85
|
+
|
|
86
|
+
if not parts:
|
|
87
|
+
parts.append(Part.text_part(""))
|
|
88
|
+
return Content(role=Role(msg.role), parts=parts)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _param_to_content(msg: dict[str, Any]) -> Content:
|
|
92
|
+
role = Role(msg["role"])
|
|
93
|
+
raw_content = msg.get("content", "")
|
|
94
|
+
|
|
95
|
+
parts: list[Part] = []
|
|
96
|
+
|
|
97
|
+
if isinstance(raw_content, str):
|
|
98
|
+
parts.append(Part.text_part(raw_content))
|
|
99
|
+
elif isinstance(raw_content, list):
|
|
100
|
+
for block in raw_content:
|
|
101
|
+
block_type = block.get("type", "text")
|
|
102
|
+
if block_type == "text":
|
|
103
|
+
parts.append(Part.text_part(block["text"]))
|
|
104
|
+
elif block_type == "thinking":
|
|
105
|
+
parts.append(Part.thinking_part(block["thinking"]))
|
|
106
|
+
elif block_type == "tool_use":
|
|
107
|
+
parts.append(
|
|
108
|
+
Part.tool_call_part(
|
|
109
|
+
ToolCall(
|
|
110
|
+
id=block["id"],
|
|
111
|
+
name=block["name"],
|
|
112
|
+
arguments=block.get("input", {}),
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
elif block_type == "tool_result":
|
|
117
|
+
result_content = block.get("content", "")
|
|
118
|
+
if isinstance(result_content, list):
|
|
119
|
+
# Extract text from content blocks
|
|
120
|
+
texts = [
|
|
121
|
+
b["text"] for b in result_content if b.get("type") == "text"
|
|
122
|
+
]
|
|
123
|
+
result_content = " ".join(texts) if texts else ""
|
|
124
|
+
parts.append(
|
|
125
|
+
Part.tool_result_part(
|
|
126
|
+
ToolResult(
|
|
127
|
+
tool_call_id=block["tool_use_id"],
|
|
128
|
+
content=result_content,
|
|
129
|
+
is_error=block.get("is_error", False),
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if not parts:
|
|
135
|
+
parts.append(Part.text_part(""))
|
|
136
|
+
|
|
137
|
+
return Content(role=role, parts=parts)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# -- Private: from internal ---------------------------------------------------
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _parts_to_blocks(parts: list[Part]) -> list[dict[str, Any]]:
|
|
144
|
+
blocks: list[dict[str, Any]] = []
|
|
145
|
+
for p in parts:
|
|
146
|
+
if p.part_type == PartType.TEXT:
|
|
147
|
+
blocks.append({"type": "text", "text": p.text or ""})
|
|
148
|
+
elif p.part_type == PartType.THINKING:
|
|
149
|
+
blocks.append({"type": "thinking", "thinking": p.text or ""})
|
|
150
|
+
elif p.part_type == PartType.TOOL_CALL:
|
|
151
|
+
tc = p.tool_call
|
|
152
|
+
blocks.append(
|
|
153
|
+
{
|
|
154
|
+
"type": "tool_use",
|
|
155
|
+
"id": tc.id,
|
|
156
|
+
"name": tc.name,
|
|
157
|
+
"input": tc.arguments,
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
elif p.part_type == PartType.TOOL_RESULT:
|
|
161
|
+
tr = p.tool_result
|
|
162
|
+
blocks.append(
|
|
163
|
+
{
|
|
164
|
+
"type": "tool_result",
|
|
165
|
+
"tool_use_id": tr.tool_call_id,
|
|
166
|
+
"content": tr.content
|
|
167
|
+
if isinstance(tr.content, str)
|
|
168
|
+
else str(tr.content),
|
|
169
|
+
"is_error": tr.is_error,
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
return blocks
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Convert between Google GenAI Content/Part types and internal schema."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from google.genai import types as genai_types
|
|
8
|
+
|
|
9
|
+
from switch_trust.eval.common.schema import (
|
|
10
|
+
Content,
|
|
11
|
+
Message,
|
|
12
|
+
Part,
|
|
13
|
+
PartType,
|
|
14
|
+
Role,
|
|
15
|
+
ToolCall,
|
|
16
|
+
ToolResult,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
_ROLE_MAP = {"user": Role.USER, "model": Role.ASSISTANT}
|
|
20
|
+
_ROLE_MAP_REVERSE = {Role.USER: "user", Role.ASSISTANT: "model"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# -- To internal --------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def to_content(
|
|
27
|
+
content: dict[str, Any] | genai_types.Content,
|
|
28
|
+
) -> Content:
|
|
29
|
+
"""Convert a GenAI Content (object or dict) to internal Content."""
|
|
30
|
+
if isinstance(content, genai_types.Content):
|
|
31
|
+
return _content_obj_to_internal(content)
|
|
32
|
+
return _content_dict_to_internal(content)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def to_message(
|
|
36
|
+
content: dict[str, Any] | genai_types.Content,
|
|
37
|
+
) -> Message:
|
|
38
|
+
return Message(content=to_content(content))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# -- From internal ------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def from_content(content: Content) -> dict[str, Any]:
|
|
45
|
+
"""Convert internal Content to a GenAI-compatible dict."""
|
|
46
|
+
role = _ROLE_MAP_REVERSE.get(content.role, "user")
|
|
47
|
+
parts = [_part_to_genai(p) for p in content.parts]
|
|
48
|
+
return {"role": role, "parts": parts}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def from_message(message: Message) -> dict[str, Any]:
|
|
52
|
+
return from_content(message.content)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def from_content_obj(content: Content) -> genai_types.Content:
|
|
56
|
+
"""Convert internal Content to a GenAI Content object."""
|
|
57
|
+
return genai_types.Content(**from_content(content))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def from_message_obj(message: Message) -> genai_types.Content:
|
|
61
|
+
return from_content_obj(message.content)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# -- Private: to internal -----------------------------------------------------
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _content_obj_to_internal(
|
|
68
|
+
content: genai_types.Content,
|
|
69
|
+
) -> Content:
|
|
70
|
+
role = _ROLE_MAP.get(content.role, Role.USER)
|
|
71
|
+
parts = [_genai_part_to_internal(p) for p in (content.parts or [])]
|
|
72
|
+
if not parts:
|
|
73
|
+
parts = [Part.text_part("")]
|
|
74
|
+
return Content(role=role, parts=parts)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _content_dict_to_internal(d: dict[str, Any]) -> Content:
|
|
78
|
+
role = _ROLE_MAP.get(d.get("role", "user"), Role.USER)
|
|
79
|
+
parts: list[Part] = []
|
|
80
|
+
for p in d.get("parts", []):
|
|
81
|
+
if isinstance(p, str):
|
|
82
|
+
parts.append(Part.text_part(p))
|
|
83
|
+
elif isinstance(p, dict):
|
|
84
|
+
parts.append(_genai_part_dict_to_internal(p))
|
|
85
|
+
else:
|
|
86
|
+
parts.append(_genai_part_to_internal(p))
|
|
87
|
+
if not parts:
|
|
88
|
+
parts = [Part.text_part("")]
|
|
89
|
+
return Content(role=role, parts=parts)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _genai_part_to_internal(part: genai_types.Part) -> Part:
|
|
93
|
+
if part.thought and part.text is not None:
|
|
94
|
+
return Part.thinking_part(part.text)
|
|
95
|
+
if part.text is not None:
|
|
96
|
+
return Part.text_part(part.text)
|
|
97
|
+
if part.function_call is not None:
|
|
98
|
+
fc = part.function_call
|
|
99
|
+
return Part.tool_call_part(
|
|
100
|
+
ToolCall(
|
|
101
|
+
id=fc.id or "",
|
|
102
|
+
name=fc.name or "",
|
|
103
|
+
arguments=dict(fc.args) if fc.args else {},
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
if part.function_response is not None:
|
|
107
|
+
fr = part.function_response
|
|
108
|
+
return Part.tool_result_part(
|
|
109
|
+
ToolResult(
|
|
110
|
+
tool_call_id=fr.id or "",
|
|
111
|
+
content=dict(fr.response) if fr.response else "",
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
return Part.text_part("")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _genai_part_dict_to_internal(d: dict[str, Any]) -> Part:
|
|
118
|
+
if "thought" in d and d.get("thought") and "text" in d:
|
|
119
|
+
return Part.thinking_part(d["text"])
|
|
120
|
+
if "text" in d:
|
|
121
|
+
return Part.text_part(d["text"])
|
|
122
|
+
if "function_call" in d:
|
|
123
|
+
fc = d["function_call"]
|
|
124
|
+
return Part.tool_call_part(
|
|
125
|
+
ToolCall(
|
|
126
|
+
id=fc.get("id", ""),
|
|
127
|
+
name=fc.get("name", ""),
|
|
128
|
+
arguments=fc.get("args", {}),
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
if "function_response" in d:
|
|
132
|
+
fr = d["function_response"]
|
|
133
|
+
return Part.tool_result_part(
|
|
134
|
+
ToolResult(
|
|
135
|
+
tool_call_id=fr.get("id", ""),
|
|
136
|
+
content=fr.get("response", ""),
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
return Part.text_part("")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# -- Private: from internal ---------------------------------------------------
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _part_to_genai(part: Part) -> dict[str, Any]:
|
|
146
|
+
if part.part_type == PartType.THINKING:
|
|
147
|
+
return {"text": part.text or "", "thought": True}
|
|
148
|
+
if part.part_type == PartType.TEXT:
|
|
149
|
+
return {"text": part.text or ""}
|
|
150
|
+
if part.part_type == PartType.TOOL_CALL:
|
|
151
|
+
tc = part.tool_call
|
|
152
|
+
return {
|
|
153
|
+
"function_call": {
|
|
154
|
+
"id": tc.id,
|
|
155
|
+
"name": tc.name,
|
|
156
|
+
"args": tc.arguments,
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
if part.part_type == PartType.TOOL_RESULT:
|
|
160
|
+
tr = part.tool_result
|
|
161
|
+
response = (
|
|
162
|
+
tr.content if isinstance(tr.content, dict) else {"result": tr.content}
|
|
163
|
+
)
|
|
164
|
+
return {
|
|
165
|
+
"function_response": {
|
|
166
|
+
"id": tr.tool_call_id,
|
|
167
|
+
"name": "",
|
|
168
|
+
"response": response,
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
return {"text": ""}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Convert between OpenAI ChatCompletion messages and internal schema."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from openai.types.chat import ChatCompletionMessage, ChatCompletionMessageParam
|
|
9
|
+
|
|
10
|
+
from switch_trust.eval.common.schema import (
|
|
11
|
+
Content,
|
|
12
|
+
Message,
|
|
13
|
+
Part,
|
|
14
|
+
PartType,
|
|
15
|
+
Role,
|
|
16
|
+
ToolCall,
|
|
17
|
+
ToolResult,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# -- To internal --------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def to_content(message: dict[str, Any] | ChatCompletionMessage) -> Content:
|
|
24
|
+
"""Convert an OpenAI message (dict param or response object) to Content."""
|
|
25
|
+
if isinstance(message, ChatCompletionMessage):
|
|
26
|
+
return _response_to_content(message)
|
|
27
|
+
return _param_to_content(message)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def to_message(message: dict[str, Any] | ChatCompletionMessage) -> Message:
|
|
31
|
+
return Message(content=to_content(message))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# -- From internal ------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def from_content(content: Content) -> dict[str, Any]:
|
|
38
|
+
"""Convert internal Content to an OpenAI message param dict."""
|
|
39
|
+
role = content.role.value
|
|
40
|
+
|
|
41
|
+
if role == "system":
|
|
42
|
+
return _content_to_system(content)
|
|
43
|
+
if role == "user":
|
|
44
|
+
return _content_to_user(content)
|
|
45
|
+
return _content_to_assistant_or_tool(content)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def from_message(message: Message) -> dict[str, Any]:
|
|
49
|
+
return from_content(message.content)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def from_content_obj(content: Content) -> ChatCompletionMessageParam:
|
|
53
|
+
"""Convert internal Content to a typed OpenAI message param."""
|
|
54
|
+
return from_content(content) # type: ignore[return-value]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def from_message_obj(message: Message) -> ChatCompletionMessageParam:
|
|
58
|
+
return from_content_obj(message.content)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# -- Private: to internal -----------------------------------------------------
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _response_to_content(msg: ChatCompletionMessage) -> Content:
|
|
65
|
+
parts: list[Part] = []
|
|
66
|
+
if msg.content:
|
|
67
|
+
parts.append(Part.text_part(msg.content))
|
|
68
|
+
if msg.tool_calls:
|
|
69
|
+
for tc in msg.tool_calls:
|
|
70
|
+
args = (
|
|
71
|
+
json.loads(tc.function.arguments)
|
|
72
|
+
if isinstance(tc.function.arguments, str)
|
|
73
|
+
else tc.function.arguments
|
|
74
|
+
)
|
|
75
|
+
parts.append(
|
|
76
|
+
Part.tool_call_part(
|
|
77
|
+
ToolCall(
|
|
78
|
+
id=tc.id,
|
|
79
|
+
name=tc.function.name,
|
|
80
|
+
arguments=args,
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
if not parts:
|
|
85
|
+
parts.append(Part.text_part(""))
|
|
86
|
+
return Content(role=Role.ASSISTANT, parts=parts)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _param_to_content(msg: dict[str, Any]) -> Content:
|
|
90
|
+
role_str = msg["role"]
|
|
91
|
+
role = Role(role_str) if role_str != "tool" else Role.USER
|
|
92
|
+
|
|
93
|
+
parts: list[Part] = []
|
|
94
|
+
|
|
95
|
+
if role_str == "tool":
|
|
96
|
+
parts.append(
|
|
97
|
+
Part.tool_result_part(
|
|
98
|
+
ToolResult(
|
|
99
|
+
tool_call_id=msg["tool_call_id"],
|
|
100
|
+
content=msg.get("content", ""),
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
return Content(role=Role.USER, parts=parts)
|
|
105
|
+
|
|
106
|
+
content = msg.get("content")
|
|
107
|
+
if isinstance(content, str):
|
|
108
|
+
parts.append(Part.text_part(content))
|
|
109
|
+
elif isinstance(content, list):
|
|
110
|
+
for item in content:
|
|
111
|
+
if item.get("type") == "text":
|
|
112
|
+
parts.append(Part.text_part(item["text"]))
|
|
113
|
+
|
|
114
|
+
tool_calls = msg.get("tool_calls")
|
|
115
|
+
if tool_calls:
|
|
116
|
+
for tc in tool_calls:
|
|
117
|
+
func = tc["function"] if isinstance(tc, dict) else tc
|
|
118
|
+
args = func["arguments"]
|
|
119
|
+
if isinstance(args, str):
|
|
120
|
+
args = json.loads(args)
|
|
121
|
+
parts.append(
|
|
122
|
+
Part.tool_call_part(
|
|
123
|
+
ToolCall(
|
|
124
|
+
id=tc["id"],
|
|
125
|
+
name=func["name"],
|
|
126
|
+
arguments=args,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if not parts:
|
|
132
|
+
parts.append(Part.text_part(""))
|
|
133
|
+
|
|
134
|
+
return Content(role=role, parts=parts)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# -- Private: from internal ---------------------------------------------------
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _content_to_system(content: Content) -> dict[str, Any]:
|
|
141
|
+
text = _collect_text(content)
|
|
142
|
+
return {"role": "system", "content": text}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _content_to_user(content: Content) -> dict[str, Any] | list[dict[str, Any]]:
|
|
146
|
+
# If content has tool results, emit them as separate tool messages.
|
|
147
|
+
tool_results = [p for p in content.parts if p.part_type == PartType.TOOL_RESULT]
|
|
148
|
+
if tool_results:
|
|
149
|
+
messages = []
|
|
150
|
+
for p in tool_results:
|
|
151
|
+
tr = p.tool_result
|
|
152
|
+
result_content = (
|
|
153
|
+
tr.content if isinstance(tr.content, str) else json.dumps(tr.content)
|
|
154
|
+
)
|
|
155
|
+
messages.append(
|
|
156
|
+
{
|
|
157
|
+
"role": "tool",
|
|
158
|
+
"tool_call_id": tr.tool_call_id,
|
|
159
|
+
"content": result_content,
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
return messages if len(messages) > 1 else messages[0]
|
|
163
|
+
|
|
164
|
+
text = _collect_text(content)
|
|
165
|
+
return {"role": "user", "content": text}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _content_to_assistant_or_tool(content: Content) -> dict[str, Any]:
|
|
169
|
+
text_parts = [p for p in content.parts if p.part_type == PartType.TEXT]
|
|
170
|
+
tool_call_parts = [p for p in content.parts if p.part_type == PartType.TOOL_CALL]
|
|
171
|
+
|
|
172
|
+
msg: dict[str, Any] = {"role": "assistant"}
|
|
173
|
+
msg["content"] = (
|
|
174
|
+
" ".join(p.text for p in text_parts if p.text) if text_parts else None
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if tool_call_parts:
|
|
178
|
+
msg["tool_calls"] = []
|
|
179
|
+
for p in tool_call_parts:
|
|
180
|
+
tc = p.tool_call
|
|
181
|
+
msg["tool_calls"].append(
|
|
182
|
+
{
|
|
183
|
+
"id": tc.id,
|
|
184
|
+
"type": "function",
|
|
185
|
+
"function": {
|
|
186
|
+
"name": tc.name,
|
|
187
|
+
"arguments": json.dumps(tc.arguments),
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return msg
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _collect_text(content: Content) -> str:
|
|
196
|
+
return " ".join(
|
|
197
|
+
p.text for p in content.parts if p.part_type == PartType.TEXT and p.text
|
|
198
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
_REDACT_PATTERNS = re.compile(
|
|
6
|
+
r"("
|
|
7
|
+
r"Bearer\s+[A-Za-z0-9\-_.~+/]+=*"
|
|
8
|
+
r"|sk-[A-Za-z0-9]{10,}"
|
|
9
|
+
r"|key-[A-Za-z0-9]{10,}"
|
|
10
|
+
r"|pat-[A-Za-z0-9]{10,}"
|
|
11
|
+
r"|AIza[A-Za-z0-9_\-]{35,}"
|
|
12
|
+
r"|xox[bspra]-[A-Za-z0-9\-]+"
|
|
13
|
+
r"|ghp_[A-Za-z0-9]{36,}"
|
|
14
|
+
r"|-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----"
|
|
15
|
+
r"[\s\S]*?-----END"
|
|
16
|
+
r")",
|
|
17
|
+
re.IGNORECASE,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_REPLACEMENT = "[REDACTED]"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def redact(text: str) -> str:
|
|
24
|
+
"""Replace any known secret pattern in ``text`` with ``[REDACTED]``."""
|
|
25
|
+
return _REDACT_PATTERNS.sub(_REPLACEMENT, text)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RedactingFilter(logging.Filter):
|
|
29
|
+
"""Scrub secrets from log records before they reach handlers."""
|
|
30
|
+
|
|
31
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
32
|
+
if isinstance(record.msg, str):
|
|
33
|
+
record.msg = redact(record.msg)
|
|
34
|
+
if record.args:
|
|
35
|
+
if isinstance(record.args, tuple):
|
|
36
|
+
record.args = tuple(
|
|
37
|
+
redact(a) if isinstance(a, str) else a for a in record.args
|
|
38
|
+
)
|
|
39
|
+
elif isinstance(record.args, dict):
|
|
40
|
+
record.args = {
|
|
41
|
+
k: redact(v) if isinstance(v, str) else v
|
|
42
|
+
for k, v in record.args.items()
|
|
43
|
+
}
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RedactingFormatter(logging.Formatter):
|
|
48
|
+
"""Wrap another formatter and scrub secrets from its rendered output.
|
|
49
|
+
|
|
50
|
+
A handler-level :class:`RedactingFilter` only sees ``record.msg`` and
|
|
51
|
+
``record.args``, so secrets that surface elsewhere — an exception message or
|
|
52
|
+
traceback (e.g. an aiohttp connect error carrying a key in the URL), or a
|
|
53
|
+
stray ``extra=`` field — reach the handler unredacted. Redacting the fully
|
|
54
|
+
formatted string closes that gap regardless of how the wrapped formatter
|
|
55
|
+
renders exceptions.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, inner: logging.Formatter) -> None:
|
|
59
|
+
super().__init__()
|
|
60
|
+
self._inner = inner
|
|
61
|
+
|
|
62
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
63
|
+
return redact(self._inner.format(record))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_NOISY_LOGGERS = [
|
|
67
|
+
"anthropic",
|
|
68
|
+
"google.genai",
|
|
69
|
+
"google.adk",
|
|
70
|
+
"google_adk",
|
|
71
|
+
"google.auth",
|
|
72
|
+
"google.api_core",
|
|
73
|
+
"google_genai.models",
|
|
74
|
+
"httpcore",
|
|
75
|
+
"httpx",
|
|
76
|
+
"urllib3",
|
|
77
|
+
"opentelemetry",
|
|
78
|
+
"litellm",
|
|
79
|
+
"LiteLLM",
|
|
80
|
+
"openai",
|
|
81
|
+
"asyncio",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def silence_noisy_loggers() -> None:
|
|
86
|
+
for name in _NOISY_LOGGERS:
|
|
87
|
+
logging.getLogger(name).setLevel(logging.WARNING)
|
|
88
|
+
logging.captureWarnings(True)
|
|
89
|
+
|
|
90
|
+
os.environ.setdefault("HF_DATASETS_DISABLE_PROGRESS_BARS", "1")
|
|
91
|
+
os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
|
|
92
|
+
os.environ.setdefault("DATASETS_VERBOSITY", "error")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s [%(filename)s:%(lineno)d]: %(message)s"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def setup_file_logging(log_path: str) -> None:
|
|
99
|
+
root = logging.getLogger()
|
|
100
|
+
root.setLevel(logging.DEBUG)
|
|
101
|
+
root.handlers.clear()
|
|
102
|
+
|
|
103
|
+
file_handler = logging.FileHandler(log_path, mode="w")
|
|
104
|
+
file_handler.setLevel(logging.DEBUG)
|
|
105
|
+
file_handler.setFormatter(logging.Formatter(LOG_FORMAT))
|
|
106
|
+
file_handler.addFilter(RedactingFilter())
|
|
107
|
+
root.addHandler(file_handler)
|
|
108
|
+
|
|
109
|
+
os.environ["LITELLM_LOG"] = "CRITICAL"
|
|
110
|
+
try:
|
|
111
|
+
import litellm # noqa: PLC0415
|
|
112
|
+
|
|
113
|
+
litellm.suppress_debug_info = True
|
|
114
|
+
except ImportError:
|
|
115
|
+
pass
|
|
116
|
+
silence_noisy_loggers()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from dataclasses_json import dataclass_json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ReferenceType(str, Enum):
|
|
8
|
+
"""The kind of object a Reference points to."""
|
|
9
|
+
|
|
10
|
+
DETECTOR = "detector"
|
|
11
|
+
EVALUATION = "evaluation"
|
|
12
|
+
EVALUATION_SUITE = "evaluation_suite"
|
|
13
|
+
MESSAGE_COLLECTION = "message_collection"
|
|
14
|
+
MODEL = "model"
|
|
15
|
+
MODEL_EVALUATION_SUITE = "model_evaluation_suite"
|
|
16
|
+
MODEL_EVALUATION_SUITE_RUN = "model_evaluation_suite_run"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass_json
|
|
20
|
+
@dataclass
|
|
21
|
+
class Reference:
|
|
22
|
+
id: str
|
|
23
|
+
name: str
|
|
24
|
+
type: ReferenceType
|
|
25
|
+
description: str | None = None
|