simpleaudit 0.2.0__tar.gz → 0.2.1__tar.gz
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.
- {simpleaudit-0.2.0/simpleaudit.egg-info → simpleaudit-0.2.1}/PKG-INFO +11 -4
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/README.md +10 -3
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/pyproject.toml +1 -1
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/cli.py +11 -2
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/experiment.py +9 -1
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/model_auditor.py +36 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1/simpleaudit.egg-info}/PKG-INFO +11 -4
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_experiment_streaming.py +73 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_model_auditor.py +169 -1
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/LICENSE +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/setup.cfg +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/__init__.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/checklist.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/context_marks.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/cross_judge.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/judges/__init__.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/judges/abstention.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/judges/binary_abstention.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/judges/checklist.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/judges/factuality.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/judges/harm.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/judges/helpfulness.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/judges/helsedir_sexhealth_no.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/judges/helsedir_sexhealth_no_rag.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/judges/safety.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/perturbations.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/reframing.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/repeated_results.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/results.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/__init__.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/arbeidstilsynet_arbeidstid.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/bullshitbench_health.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/bullshitbench_v1_v2.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/health.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/hei_refusal.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/helfo.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/helpmed.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/human_rights_education.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/human_rights_fair_trial.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/human_rights_water.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/images/blank_chart.png +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/images/illegible_label.png +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/images/injection.png +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/images/make_images.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/images/product_a.png +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/images/product_b.png +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/images/quarterly_revenue.png +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/images/truncated_axis.png +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/lanekassen.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/nav_aap.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/nb_kryss_ordning.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/rag.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/safety.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/skatteetaten.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/skatteetaten_legitimasjon.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/system_prompt.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/toll_reisegodskvote.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/ung.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/scenarios/vision_integrity.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/spans.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/utils.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/visualization/scenario_viewer.html +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/visualization/server.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/visualization/thumbnail.png +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit/visualization/visualizer.html +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit.egg-info/SOURCES.txt +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit.egg-info/dependency_links.txt +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit.egg-info/entry_points.txt +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit.egg-info/requires.txt +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/simpleaudit.egg-info/top_level.txt +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_anthropic_judge_default_path.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_audit_flow.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_basic.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_checklist.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_checklist_judge.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_checklist_pipeline.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_checklist_reframing.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_cross_judge.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_custom_prompts.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_document_expansion.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_fakes.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_file_uri.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_fragility.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_generation_params.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_json_format.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_judge_notes.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_judge_postprocess.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_judge_registry.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_judge_response_schema.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_judge_robustness_pipeline.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_perturbations.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_reframing.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_rejudge.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_repeated_experiments.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_resumable_experiments.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_scenario_data.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_scenario_pack_conventions.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_spans.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_strip_thinking.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_target_api_key.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_token_counting.py +0 -0
- {simpleaudit-0.2.0 → simpleaudit-0.2.1}/tests/test_vision_integrity_pack.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simpleaudit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Lightweight AI Safety Auditing Framework
|
|
5
5
|
Author: SimpleAudit Contributors
|
|
6
6
|
License: MIT
|
|
@@ -162,10 +162,10 @@ results.save("./my_audit_results/audit_results.json")
|
|
|
162
162
|
**💡 View results interactively:**
|
|
163
163
|
```bash
|
|
164
164
|
# Option 1: Run directly with uvx (no installation needed, requires uv)
|
|
165
|
-
uvx simpleaudit[visualize] serve --results_dir ./my_audit_results
|
|
165
|
+
uvx 'simpleaudit[visualize]' serve --results_dir ./my_audit_results
|
|
166
166
|
|
|
167
167
|
# Option 2: Install and run locally
|
|
168
|
-
pip install simpleaudit[visualize]
|
|
168
|
+
pip install 'simpleaudit[visualize]'
|
|
169
169
|
simpleaudit serve --results_dir ./my_audit_results
|
|
170
170
|
```
|
|
171
171
|
This will spin-up a local web server to explore results with scenario details. 👉 [Check for live demo.](https://simulamet-simpleauditvisualization.hf.space)
|
|
@@ -173,7 +173,14 @@ See [visualization/README.md](https://github.com/kelkalot/simpleaudit/blob/main/
|
|
|
173
173
|
|
|
174
174
|
To share results as a single self-contained HTML file (no server, no JSON upload), use `simpleaudit export-html ./audit_results.json` or the **Download HTML** button in the visualizer.
|
|
175
175
|
|
|
176
|
-
> **Note:** Option 1 requires [`uv`](https://pypi.org/project/uv/) to be installed ([install guide](https://docs.astral.sh/uv/getting-started/installation/)).
|
|
176
|
+
> **Note:** Option 1 requires [`uv`](https://pypi.org/project/uv/) to be installed ([install guide](https://docs.astral.sh/uv/getting-started/installation/)). The package spec is quoted so your shell doesn't glob-expand `[visualize]`.
|
|
177
|
+
|
|
178
|
+
<Troubleshooting>
|
|
179
|
+
If the command fails:
|
|
180
|
+
- **`Address already in use`** — port 8000 is taken. Use `--port 8080` or stop the other process.
|
|
181
|
+
- **`uvx: command not found`** — install `uv` first (`curl -LsSf https://astral.sh/uv/install.sh | sh`).
|
|
182
|
+
- **Weird import/module error** — stale cache. Run `uv cache clean` and retry.
|
|
183
|
+
</Troubleshooting>
|
|
177
184
|
|
|
178
185
|
[](https://github.com/kelkalot/simpleaudit/blob/main/simpleaudit/visualization/README.md)
|
|
179
186
|
|
|
@@ -120,10 +120,10 @@ results.save("./my_audit_results/audit_results.json")
|
|
|
120
120
|
**💡 View results interactively:**
|
|
121
121
|
```bash
|
|
122
122
|
# Option 1: Run directly with uvx (no installation needed, requires uv)
|
|
123
|
-
uvx simpleaudit[visualize] serve --results_dir ./my_audit_results
|
|
123
|
+
uvx 'simpleaudit[visualize]' serve --results_dir ./my_audit_results
|
|
124
124
|
|
|
125
125
|
# Option 2: Install and run locally
|
|
126
|
-
pip install simpleaudit[visualize]
|
|
126
|
+
pip install 'simpleaudit[visualize]'
|
|
127
127
|
simpleaudit serve --results_dir ./my_audit_results
|
|
128
128
|
```
|
|
129
129
|
This will spin-up a local web server to explore results with scenario details. 👉 [Check for live demo.](https://simulamet-simpleauditvisualization.hf.space)
|
|
@@ -131,7 +131,14 @@ See [visualization/README.md](https://github.com/kelkalot/simpleaudit/blob/main/
|
|
|
131
131
|
|
|
132
132
|
To share results as a single self-contained HTML file (no server, no JSON upload), use `simpleaudit export-html ./audit_results.json` or the **Download HTML** button in the visualizer.
|
|
133
133
|
|
|
134
|
-
> **Note:** Option 1 requires [`uv`](https://pypi.org/project/uv/) to be installed ([install guide](https://docs.astral.sh/uv/getting-started/installation/)).
|
|
134
|
+
> **Note:** Option 1 requires [`uv`](https://pypi.org/project/uv/) to be installed ([install guide](https://docs.astral.sh/uv/getting-started/installation/)). The package spec is quoted so your shell doesn't glob-expand `[visualize]`.
|
|
135
|
+
|
|
136
|
+
<Troubleshooting>
|
|
137
|
+
If the command fails:
|
|
138
|
+
- **`Address already in use`** — port 8000 is taken. Use `--port 8080` or stop the other process.
|
|
139
|
+
- **`uvx: command not found`** — install `uv` first (`curl -LsSf https://astral.sh/uv/install.sh | sh`).
|
|
140
|
+
- **Weird import/module error** — stale cache. Run `uv cache clean` and retry.
|
|
141
|
+
</Troubleshooting>
|
|
135
142
|
|
|
136
143
|
[](https://github.com/kelkalot/simpleaudit/blob/main/simpleaudit/visualization/README.md)
|
|
137
144
|
|
|
@@ -71,8 +71,17 @@ def main():
|
|
|
71
71
|
results_dir = "."
|
|
72
72
|
print("⚠️ Warning: --results_dir not specified, using current directory '.'")
|
|
73
73
|
print(" Recommended: explicitly set --results_dir to avoid confusion\n")
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
start_server(results_dir, args.host, args.port)
|
|
77
|
+
except OSError as exc:
|
|
78
|
+
if "Address already in use" in str(exc) or "Errno 48" in str(exc):
|
|
79
|
+
print(f"Error: port {args.port} is already in use.")
|
|
80
|
+
print(f"Try a different port: simpleaudit serve --results_dir {results_dir} --port 8080")
|
|
81
|
+
print("Or stop the process currently using that port.\n")
|
|
82
|
+
else:
|
|
83
|
+
raise
|
|
84
|
+
sys.exit(1)
|
|
76
85
|
elif args.command == "export-html":
|
|
77
86
|
try:
|
|
78
87
|
from .visualization.server import export_standalone_html
|
|
@@ -305,6 +305,7 @@ class AuditExperiment:
|
|
|
305
305
|
max_turns: Optional[int],
|
|
306
306
|
language: str,
|
|
307
307
|
max_workers: int,
|
|
308
|
+
on_turn: Optional[Callable[[int, int, str], None]] = None,
|
|
308
309
|
) -> AuditResults:
|
|
309
310
|
"""Execute one rep with auto-retry on ERROR. Returns the final result."""
|
|
310
311
|
attempts = 1 + self.max_retries_per_rep
|
|
@@ -316,6 +317,7 @@ class AuditExperiment:
|
|
|
316
317
|
max_turns=max_turns,
|
|
317
318
|
language=language,
|
|
318
319
|
max_workers=max_workers,
|
|
320
|
+
on_turn=on_turn,
|
|
319
321
|
)
|
|
320
322
|
if not any(r.severity == "ERROR" for r in result):
|
|
321
323
|
break
|
|
@@ -335,6 +337,7 @@ class AuditExperiment:
|
|
|
335
337
|
scenario: Dict[str, Any],
|
|
336
338
|
max_turns: Optional[int] = None,
|
|
337
339
|
language: str = "English",
|
|
340
|
+
on_turn: Optional[Callable[[int, int, str], None]] = None,
|
|
338
341
|
) -> List[AuditResult]:
|
|
339
342
|
"""Run a single scenario N times for one model.
|
|
340
343
|
|
|
@@ -348,6 +351,10 @@ class AuditExperiment:
|
|
|
348
351
|
scenario: A single scenario dict.
|
|
349
352
|
max_turns: Override for max conversation turns.
|
|
350
353
|
language: Language for probe generation.
|
|
354
|
+
on_turn: Optional callback fired at each phase boundary with
|
|
355
|
+
``(turn_index, max_turns, role)`` where role is "auditor",
|
|
356
|
+
"target", or "judge". Called synchronously from within the
|
|
357
|
+
asyncio event loop.
|
|
351
358
|
|
|
352
359
|
Returns:
|
|
353
360
|
List of :class:`AuditResult`, one per completed rep. May be
|
|
@@ -397,7 +404,8 @@ class AuditExperiment:
|
|
|
397
404
|
|
|
398
405
|
# Execute with auto-retry
|
|
399
406
|
rep_result = await self._run_single_rep(
|
|
400
|
-
merged, [scenario], max_turns, language, max_workers=1
|
|
407
|
+
merged, [scenario], max_turns, language, max_workers=1,
|
|
408
|
+
on_turn=on_turn,
|
|
401
409
|
)
|
|
402
410
|
|
|
403
411
|
# Persist
|
|
@@ -15,6 +15,7 @@ import asyncio
|
|
|
15
15
|
import json
|
|
16
16
|
import re
|
|
17
17
|
import threading
|
|
18
|
+
import traceback
|
|
18
19
|
import warnings
|
|
19
20
|
from datetime import date
|
|
20
21
|
from importlib.metadata import PackageNotFoundError
|
|
@@ -309,6 +310,7 @@ class ModelAuditor:
|
|
|
309
310
|
target_params: Optional[Dict[str, Any]] = None,
|
|
310
311
|
judge_params: Optional[Dict[str, Any]] = None,
|
|
311
312
|
auditor_params: Optional[Dict[str, Any]] = None,
|
|
313
|
+
on_turn: Optional[Callable[[int, int, str], None]] = None,
|
|
312
314
|
):
|
|
313
315
|
if max_retries < 0:
|
|
314
316
|
raise ValueError(f"max_retries must be >= 0, got {max_retries}")
|
|
@@ -319,6 +321,7 @@ class ModelAuditor:
|
|
|
319
321
|
self.json_format = json_format
|
|
320
322
|
self.max_retries = max_retries
|
|
321
323
|
self.retry_backoff = retry_backoff
|
|
324
|
+
self.on_turn = on_turn
|
|
322
325
|
self.judge_fields = judge_fields
|
|
323
326
|
self.params = params
|
|
324
327
|
self.target_params = target_params
|
|
@@ -489,6 +492,26 @@ class ModelAuditor:
|
|
|
489
492
|
message = f"[{name}] {message}"
|
|
490
493
|
tqdm.write(message)
|
|
491
494
|
|
|
495
|
+
def _fire_on_turn(
|
|
496
|
+
self,
|
|
497
|
+
turn_index: int,
|
|
498
|
+
max_turns: int,
|
|
499
|
+
role: str,
|
|
500
|
+
callback: Optional[Callable[[int, int, str], None]] = None,
|
|
501
|
+
) -> None:
|
|
502
|
+
"""Invoke an on_turn callback if set. Never raises.
|
|
503
|
+
|
|
504
|
+
Uses ``callback`` when provided, otherwise falls back to the
|
|
505
|
+
construction-time ``self.on_turn``. A raising callback is logged,
|
|
506
|
+
not propagated — a progress observer must never fail an audit.
|
|
507
|
+
"""
|
|
508
|
+
cb = callback if callback is not None else self.on_turn
|
|
509
|
+
if cb is not None:
|
|
510
|
+
try:
|
|
511
|
+
cb(turn_index, max_turns, role)
|
|
512
|
+
except Exception:
|
|
513
|
+
self._log(f"on_turn callback failed:\n{traceback.format_exc()}")
|
|
514
|
+
|
|
492
515
|
@staticmethod
|
|
493
516
|
def strip_thinking(text: str) -> str:
|
|
494
517
|
# Remove complete <think>...</think> / <thinking>...</thinking> blocks.
|
|
@@ -809,6 +832,7 @@ Evaluate this conversation and respond with this exact JSON structure:
|
|
|
809
832
|
target_params: Optional[Dict[str, Any]] = None,
|
|
810
833
|
judge_params: Optional[Dict[str, Any]] = None,
|
|
811
834
|
auditor_params: Optional[Dict[str, Any]] = None,
|
|
835
|
+
on_turn: Optional[Callable[[int, int, str], None]] = None,
|
|
812
836
|
) -> AuditResult:
|
|
813
837
|
turns = max_turns or self.max_turns
|
|
814
838
|
base = {**(self.params or {}), **(params or {})}
|
|
@@ -816,6 +840,9 @@ Evaluate this conversation and respond with this exact JSON structure:
|
|
|
816
840
|
effective_judge = {**base, **(self.judge_params or {}), **(judge_params or {})}
|
|
817
841
|
effective_auditor = {**base, **(self.auditor_params or {}), **(auditor_params or {})}
|
|
818
842
|
|
|
843
|
+
# A per-call on_turn overrides one set at construction time; either may be used.
|
|
844
|
+
effective_on_turn = on_turn if on_turn is not None else self.on_turn
|
|
845
|
+
|
|
819
846
|
mode_str = " (Parallel)" if (max_workers or 1) > 1 else ""
|
|
820
847
|
self._log(f"--- Started Scenario: {name}{mode_str} ---")
|
|
821
848
|
|
|
@@ -858,6 +885,7 @@ Evaluate this conversation and respond with this exact JSON structure:
|
|
|
858
885
|
auditor_input_tokens += a_in
|
|
859
886
|
auditor_output_tokens += a_out
|
|
860
887
|
probe = ModelAuditor.strip_thinking(probe)
|
|
888
|
+
self._fire_on_turn(turn, turns, "auditor", effective_on_turn)
|
|
861
889
|
|
|
862
890
|
probe_preview = probe[:80] + "..." if len(probe) > 80 else probe
|
|
863
891
|
self._log(f"PROBE: {probe_preview}", name=name)
|
|
@@ -885,6 +913,7 @@ Evaluate this conversation and respond with this exact JSON structure:
|
|
|
885
913
|
target_input_tokens += t_in
|
|
886
914
|
target_output_tokens += t_out
|
|
887
915
|
response = ModelAuditor.strip_thinking(response)
|
|
916
|
+
self._fire_on_turn(turn, turns, "target", effective_on_turn)
|
|
888
917
|
|
|
889
918
|
response_preview = response[:80] + "..." if len(response) > 80 else response
|
|
890
919
|
self._log(f"TARGET: {response_preview}", name=name)
|
|
@@ -927,6 +956,9 @@ Evaluate this conversation and respond with this exact JSON structure:
|
|
|
927
956
|
)
|
|
928
957
|
judge_input_tokens += j_in
|
|
929
958
|
judge_output_tokens += j_out
|
|
959
|
+
# The judge runs once after all turns complete; report it against
|
|
960
|
+
# the final turn index so consumers can place it at the end.
|
|
961
|
+
self._fire_on_turn(turns - 1, turns, "judge", effective_on_turn)
|
|
930
962
|
if fell_back and isinstance(judgment, dict):
|
|
931
963
|
judgment["judge_fallback"] = "default"
|
|
932
964
|
except Exception as exc:
|
|
@@ -989,6 +1021,7 @@ Evaluate this conversation and respond with this exact JSON structure:
|
|
|
989
1021
|
target_params: Optional[Dict[str, Any]] = None,
|
|
990
1022
|
judge_params: Optional[Dict[str, Any]] = None,
|
|
991
1023
|
auditor_params: Optional[Dict[str, Any]] = None,
|
|
1024
|
+
on_turn: Optional[Callable[[int, int, str], None]] = None,
|
|
992
1025
|
) -> AuditResults:
|
|
993
1026
|
if max_workers < 1:
|
|
994
1027
|
raise ValueError(
|
|
@@ -1058,6 +1091,7 @@ Evaluate this conversation and respond with this exact JSON structure:
|
|
|
1058
1091
|
target_params=target_params,
|
|
1059
1092
|
judge_params=judge_params,
|
|
1060
1093
|
auditor_params=auditor_params,
|
|
1094
|
+
on_turn=on_turn,
|
|
1061
1095
|
)
|
|
1062
1096
|
except Exception as exc:
|
|
1063
1097
|
# Don't let one failing scenario abort the whole batch and
|
|
@@ -1114,6 +1148,7 @@ Evaluate this conversation and respond with this exact JSON structure:
|
|
|
1114
1148
|
target_params: Optional[Dict[str, Any]] = None,
|
|
1115
1149
|
judge_params: Optional[Dict[str, Any]] = None,
|
|
1116
1150
|
auditor_params: Optional[Dict[str, Any]] = None,
|
|
1151
|
+
on_turn: Optional[Callable[[int, int, str], None]] = None,
|
|
1117
1152
|
) -> AuditResults:
|
|
1118
1153
|
try:
|
|
1119
1154
|
asyncio.get_running_loop()
|
|
@@ -1128,6 +1163,7 @@ Evaluate this conversation and respond with this exact JSON structure:
|
|
|
1128
1163
|
target_params=target_params,
|
|
1129
1164
|
judge_params=judge_params,
|
|
1130
1165
|
auditor_params=auditor_params,
|
|
1166
|
+
on_turn=on_turn,
|
|
1131
1167
|
)
|
|
1132
1168
|
)
|
|
1133
1169
|
msg = "ModelAuditor.run() cannot be called from an active event loop. Use await <object>.run_async()."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simpleaudit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Lightweight AI Safety Auditing Framework
|
|
5
5
|
Author: SimpleAudit Contributors
|
|
6
6
|
License: MIT
|
|
@@ -162,10 +162,10 @@ results.save("./my_audit_results/audit_results.json")
|
|
|
162
162
|
**💡 View results interactively:**
|
|
163
163
|
```bash
|
|
164
164
|
# Option 1: Run directly with uvx (no installation needed, requires uv)
|
|
165
|
-
uvx simpleaudit[visualize] serve --results_dir ./my_audit_results
|
|
165
|
+
uvx 'simpleaudit[visualize]' serve --results_dir ./my_audit_results
|
|
166
166
|
|
|
167
167
|
# Option 2: Install and run locally
|
|
168
|
-
pip install simpleaudit[visualize]
|
|
168
|
+
pip install 'simpleaudit[visualize]'
|
|
169
169
|
simpleaudit serve --results_dir ./my_audit_results
|
|
170
170
|
```
|
|
171
171
|
This will spin-up a local web server to explore results with scenario details. 👉 [Check for live demo.](https://simulamet-simpleauditvisualization.hf.space)
|
|
@@ -173,7 +173,14 @@ See [visualization/README.md](https://github.com/kelkalot/simpleaudit/blob/main/
|
|
|
173
173
|
|
|
174
174
|
To share results as a single self-contained HTML file (no server, no JSON upload), use `simpleaudit export-html ./audit_results.json` or the **Download HTML** button in the visualizer.
|
|
175
175
|
|
|
176
|
-
> **Note:** Option 1 requires [`uv`](https://pypi.org/project/uv/) to be installed ([install guide](https://docs.astral.sh/uv/getting-started/installation/)).
|
|
176
|
+
> **Note:** Option 1 requires [`uv`](https://pypi.org/project/uv/) to be installed ([install guide](https://docs.astral.sh/uv/getting-started/installation/)). The package spec is quoted so your shell doesn't glob-expand `[visualize]`.
|
|
177
|
+
|
|
178
|
+
<Troubleshooting>
|
|
179
|
+
If the command fails:
|
|
180
|
+
- **`Address already in use`** — port 8000 is taken. Use `--port 8080` or stop the other process.
|
|
181
|
+
- **`uvx: command not found`** — install `uv` first (`curl -LsSf https://astral.sh/uv/install.sh | sh`).
|
|
182
|
+
- **Weird import/module error** — stale cache. Run `uv cache clean` and retry.
|
|
183
|
+
</Troubleshooting>
|
|
177
184
|
|
|
178
185
|
[](https://github.com/kelkalot/simpleaudit/blob/main/simpleaudit/visualization/README.md)
|
|
179
186
|
|
|
@@ -555,3 +555,76 @@ class TestExperimentEvent:
|
|
|
555
555
|
assert e.rep_index == 2
|
|
556
556
|
assert e.total_reps == 5
|
|
557
557
|
assert e.result.severity == "pass"
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# ---------------------------------------------------------------------------
|
|
561
|
+
# on_turn forwarding through run_scenario_reps
|
|
562
|
+
# ---------------------------------------------------------------------------
|
|
563
|
+
|
|
564
|
+
class TestOnTurnForwarding:
|
|
565
|
+
"""Tests that on_turn is forwarded through run_scenario_reps."""
|
|
566
|
+
|
|
567
|
+
def test_on_turn_forwarded_to_each_rep(self):
|
|
568
|
+
"""on_turn should be called for each rep's execution."""
|
|
569
|
+
calls = []
|
|
570
|
+
|
|
571
|
+
def on_turn(turn_index, max_turns, role):
|
|
572
|
+
calls.append((turn_index, max_turns, role))
|
|
573
|
+
|
|
574
|
+
with patch.object(ModelAuditor, '_create_anyllm_client', return_value=MagicMock()):
|
|
575
|
+
exp = AuditExperiment(
|
|
576
|
+
models=[{"model": "test-model", "provider": "openai"}],
|
|
577
|
+
judge_model="judge",
|
|
578
|
+
judge_provider="openai",
|
|
579
|
+
show_progress=False,
|
|
580
|
+
n_repetitions=2,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
# Mock run_async to capture on_turn
|
|
584
|
+
async def fake_run_async(self_a, scenarios, **kwargs):
|
|
585
|
+
# Verify on_turn was passed
|
|
586
|
+
assert 'on_turn' in kwargs
|
|
587
|
+
if kwargs['on_turn']:
|
|
588
|
+
# Simulate a single turn with target phase
|
|
589
|
+
kwargs['on_turn'](0, 1, "target")
|
|
590
|
+
return _make_results(["pass"])
|
|
591
|
+
|
|
592
|
+
with patch.object(ModelAuditor, 'run_async', new=fake_run_async):
|
|
593
|
+
results = asyncio.run(exp.run_scenario_reps(
|
|
594
|
+
model_index=0,
|
|
595
|
+
scenario={"name": "test", "description": "desc"},
|
|
596
|
+
on_turn=on_turn,
|
|
597
|
+
))
|
|
598
|
+
|
|
599
|
+
# Should have created 2 reps
|
|
600
|
+
assert len(results) == 2
|
|
601
|
+
# on_turn should have been called (at least once per rep)
|
|
602
|
+
assert len(calls) >= 2
|
|
603
|
+
|
|
604
|
+
def test_on_turn_not_forwarded_when_none(self):
|
|
605
|
+
"""When on_turn is None, it should not cause errors."""
|
|
606
|
+
captured_kwargs = []
|
|
607
|
+
|
|
608
|
+
async def fake_run_async(self_a, scenarios, **kwargs):
|
|
609
|
+
captured_kwargs.append(kwargs)
|
|
610
|
+
return _make_results(["pass"])
|
|
611
|
+
|
|
612
|
+
with patch.object(ModelAuditor, '_create_anyllm_client', return_value=MagicMock()), \
|
|
613
|
+
patch.object(ModelAuditor, 'run_async', new=fake_run_async):
|
|
614
|
+
|
|
615
|
+
exp = AuditExperiment(
|
|
616
|
+
models=[{"model": "test-model", "provider": "openai"}],
|
|
617
|
+
judge_model="judge",
|
|
618
|
+
judge_provider="openai",
|
|
619
|
+
show_progress=False,
|
|
620
|
+
n_repetitions=1,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
asyncio.run(exp.run_scenario_reps(
|
|
624
|
+
model_index=0,
|
|
625
|
+
scenario={"name": "test", "description": "desc"},
|
|
626
|
+
))
|
|
627
|
+
|
|
628
|
+
# on_turn should not be in kwargs or should be None
|
|
629
|
+
for kwargs in captured_kwargs:
|
|
630
|
+
assert 'on_turn' not in kwargs or kwargs['on_turn'] is None
|
|
@@ -6,7 +6,7 @@ Run with: pytest tests/test_model_auditor.py -v
|
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import pytest
|
|
9
|
-
from unittest.mock import Mock, patch, MagicMock
|
|
9
|
+
from unittest.mock import AsyncMock, Mock, patch, MagicMock
|
|
10
10
|
|
|
11
11
|
from simpleaudit import ModelAuditor, get_scenarios, list_scenario_packs
|
|
12
12
|
# Check for optional provider dependencies
|
|
@@ -354,3 +354,171 @@ def test_model_auditor_judge_fields_none_keeps_default():
|
|
|
354
354
|
)
|
|
355
355
|
assert auditor.judge_fields is None
|
|
356
356
|
assert auditor.judge_response_schema is None # no explicit schema set
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
# on_turn callback
|
|
361
|
+
# ---------------------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
class TestOnTurnCallback:
|
|
364
|
+
"""Tests for the on_turn callback in run_scenario."""
|
|
365
|
+
|
|
366
|
+
def _make_auditor(self, **kwargs):
|
|
367
|
+
"""Create a ModelAuditor with mocked LLM clients."""
|
|
368
|
+
with patch.object(ModelAuditor, "_create_anyllm_client", return_value=MagicMock()):
|
|
369
|
+
return ModelAuditor(
|
|
370
|
+
model="test-model",
|
|
371
|
+
provider="openai",
|
|
372
|
+
judge_model="judge-model",
|
|
373
|
+
judge_provider="openai",
|
|
374
|
+
api_key="test-key",
|
|
375
|
+
show_progress=False,
|
|
376
|
+
verbose=False,
|
|
377
|
+
**kwargs,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def test_on_turn_called_at_each_phase_exact_sequence(self):
|
|
381
|
+
"""on_turn fires in the exact phase order with correct turn indices.
|
|
382
|
+
|
|
383
|
+
With a test_prompt, turn 0 skips probe generation (auditor), so the
|
|
384
|
+
sequence is: turn0 target -> turn1 auditor -> turn1 target -> judge.
|
|
385
|
+
The judge runs once after all turns, reported against the final index.
|
|
386
|
+
"""
|
|
387
|
+
calls = []
|
|
388
|
+
|
|
389
|
+
def on_turn(turn_index, max_turns, role):
|
|
390
|
+
calls.append((turn_index, max_turns, role))
|
|
391
|
+
|
|
392
|
+
auditor = self._make_auditor()
|
|
393
|
+
|
|
394
|
+
with patch.object(ModelAuditor, "_generate_probe_async", new_callable=AsyncMock) as mock_probe, \
|
|
395
|
+
patch.object(ModelAuditor, "_call_async", new_callable=AsyncMock) as mock_call, \
|
|
396
|
+
patch.object(ModelAuditor, "_judge_conversation_async", new_callable=AsyncMock) as mock_judge:
|
|
397
|
+
|
|
398
|
+
mock_probe.return_value = ("probe text", 10, 20)
|
|
399
|
+
mock_call.return_value = ("response text", 30, 40)
|
|
400
|
+
mock_judge.return_value = ({"severity": "pass"}, 50, 60)
|
|
401
|
+
|
|
402
|
+
asyncio.run(auditor.run_scenario(
|
|
403
|
+
name="test-scenario",
|
|
404
|
+
description="test description",
|
|
405
|
+
expected_behavior=["should do X"],
|
|
406
|
+
test_prompt="initial prompt",
|
|
407
|
+
max_turns=2,
|
|
408
|
+
language="English",
|
|
409
|
+
on_turn=on_turn,
|
|
410
|
+
))
|
|
411
|
+
|
|
412
|
+
assert calls == [
|
|
413
|
+
(0, 2, "target"), # turn 0: test_prompt sent verbatim, no probe
|
|
414
|
+
(1, 2, "auditor"), # turn 1: probe generated
|
|
415
|
+
(1, 2, "target"), # turn 1: target responds
|
|
416
|
+
(1, 2, "judge"), # judge runs once after all turns
|
|
417
|
+
]
|
|
418
|
+
|
|
419
|
+
def test_on_turn_set_in_init_fires_without_per_call_arg(self):
|
|
420
|
+
"""A callback set at construction time must fire even when run_scenario
|
|
421
|
+
is called without an explicit on_turn argument (regression: the local
|
|
422
|
+
param previously shadowed self.on_turn, so init-set callbacks never ran)."""
|
|
423
|
+
calls = []
|
|
424
|
+
|
|
425
|
+
def on_turn(turn_index, max_turns, role):
|
|
426
|
+
calls.append((turn_index, max_turns, role))
|
|
427
|
+
|
|
428
|
+
auditor = self._make_auditor(on_turn=on_turn)
|
|
429
|
+
|
|
430
|
+
with patch.object(ModelAuditor, "_generate_probe_async", new_callable=AsyncMock) as mock_probe, \
|
|
431
|
+
patch.object(ModelAuditor, "_call_async", new_callable=AsyncMock) as mock_call, \
|
|
432
|
+
patch.object(ModelAuditor, "_judge_conversation_async", new_callable=AsyncMock) as mock_judge:
|
|
433
|
+
|
|
434
|
+
mock_probe.return_value = ("probe", 10, 20)
|
|
435
|
+
mock_call.return_value = ("response", 30, 40)
|
|
436
|
+
mock_judge.return_value = ({"severity": "pass"}, 50, 60)
|
|
437
|
+
|
|
438
|
+
# No on_turn passed here — it must come from __init__.
|
|
439
|
+
asyncio.run(auditor.run_scenario(
|
|
440
|
+
name="test",
|
|
441
|
+
description="desc",
|
|
442
|
+
max_turns=1,
|
|
443
|
+
language="English",
|
|
444
|
+
))
|
|
445
|
+
|
|
446
|
+
assert len(calls) >= 1
|
|
447
|
+
assert any(role == "target" for _, _, role in calls)
|
|
448
|
+
|
|
449
|
+
def test_on_turn_per_call_overrides_init(self):
|
|
450
|
+
"""A per-call on_turn takes precedence over the construction-time one."""
|
|
451
|
+
init_calls = []
|
|
452
|
+
call_calls = []
|
|
453
|
+
|
|
454
|
+
auditor = self._make_auditor(on_turn=lambda *a: init_calls.append(a))
|
|
455
|
+
|
|
456
|
+
with patch.object(ModelAuditor, "_generate_probe_async", new_callable=AsyncMock) as mock_probe, \
|
|
457
|
+
patch.object(ModelAuditor, "_call_async", new_callable=AsyncMock) as mock_call, \
|
|
458
|
+
patch.object(ModelAuditor, "_judge_conversation_async", new_callable=AsyncMock) as mock_judge:
|
|
459
|
+
|
|
460
|
+
mock_probe.return_value = ("probe", 10, 20)
|
|
461
|
+
mock_call.return_value = ("response", 30, 40)
|
|
462
|
+
mock_judge.return_value = ({"severity": "pass"}, 50, 60)
|
|
463
|
+
|
|
464
|
+
asyncio.run(auditor.run_scenario(
|
|
465
|
+
name="test",
|
|
466
|
+
description="desc",
|
|
467
|
+
max_turns=1,
|
|
468
|
+
language="English",
|
|
469
|
+
on_turn=lambda *a: call_calls.append(a),
|
|
470
|
+
))
|
|
471
|
+
|
|
472
|
+
assert len(call_calls) >= 1
|
|
473
|
+
assert init_calls == [] # init callback must NOT fire when per-call given
|
|
474
|
+
|
|
475
|
+
def test_on_turn_not_called_when_none(self):
|
|
476
|
+
"""When on_turn is None, no callback should fire (no error)."""
|
|
477
|
+
auditor = self._make_auditor()
|
|
478
|
+
|
|
479
|
+
with patch.object(ModelAuditor, "_generate_probe_async", new_callable=AsyncMock) as mock_probe, \
|
|
480
|
+
patch.object(ModelAuditor, "_call_async", new_callable=AsyncMock) as mock_call, \
|
|
481
|
+
patch.object(ModelAuditor, "_judge_conversation_async", new_callable=AsyncMock) as mock_judge:
|
|
482
|
+
|
|
483
|
+
mock_probe.return_value = ("probe", 10, 20)
|
|
484
|
+
mock_call.return_value = ("response", 30, 40)
|
|
485
|
+
mock_judge.return_value = ({"severity": "pass"}, 50, 60)
|
|
486
|
+
|
|
487
|
+
# Should not raise
|
|
488
|
+
result = asyncio.run(auditor.run_scenario(
|
|
489
|
+
name="test",
|
|
490
|
+
description="desc",
|
|
491
|
+
max_turns=1,
|
|
492
|
+
language="English",
|
|
493
|
+
on_turn=None,
|
|
494
|
+
))
|
|
495
|
+
|
|
496
|
+
def test_on_turn_receives_correct_turn_index(self):
|
|
497
|
+
"""turn_index should increment correctly across turns."""
|
|
498
|
+
calls = []
|
|
499
|
+
|
|
500
|
+
def on_turn(turn_index, max_turns, role):
|
|
501
|
+
calls.append((turn_index, role))
|
|
502
|
+
|
|
503
|
+
auditor = self._make_auditor()
|
|
504
|
+
|
|
505
|
+
with patch.object(ModelAuditor, "_generate_probe_async", new_callable=AsyncMock) as mock_probe, \
|
|
506
|
+
patch.object(ModelAuditor, "_call_async", new_callable=AsyncMock) as mock_call, \
|
|
507
|
+
patch.object(ModelAuditor, "_judge_conversation_async", new_callable=AsyncMock) as mock_judge:
|
|
508
|
+
|
|
509
|
+
mock_probe.return_value = ("probe", 10, 20)
|
|
510
|
+
mock_call.return_value = ("response", 30, 40)
|
|
511
|
+
mock_judge.return_value = ({"severity": "pass"}, 50, 60)
|
|
512
|
+
|
|
513
|
+
asyncio.run(auditor.run_scenario(
|
|
514
|
+
name="test",
|
|
515
|
+
description="desc",
|
|
516
|
+
max_turns=3,
|
|
517
|
+
language="English",
|
|
518
|
+
on_turn=on_turn,
|
|
519
|
+
))
|
|
520
|
+
|
|
521
|
+
# Extract turn indices for target calls
|
|
522
|
+
target_calls = [(idx, role) for idx, role in calls if role == "target"]
|
|
523
|
+
turn_indices = [idx for idx, _ in target_calls]
|
|
524
|
+
assert turn_indices == [0, 1, 2] # One target call per turn
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|