agentprobe-testing 1.2.0__tar.gz → 1.4.0__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.
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/PKG-INFO +1 -1
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/__init__.py +1 -1
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/priority.py +14 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/quickstart.py +26 -1
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/registry.py +1 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/report.py +14 -2
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/PKG-INFO +1 -1
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/SOURCES.txt +1 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/pyproject.toml +1 -1
- agentprobe_testing-1.4.0/tests/test_injection_mode.py +80 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_priority.py +49 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/LICENSE +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/README.md +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/agents/__init__.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/agents/base.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/agents/browser_agent.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/agents/rule_based.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/agents/scripted.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/agents/target_agent.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/agreement.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/classifier.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/cli.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/diff.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domain.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/__init__.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/__init__.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/agent.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/clean.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/complex_agent.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/decoy.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/domain.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/entities.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/injector_prompt.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/rule_based_agent.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/scenarios.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/split.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/tools.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/trap.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/__init__.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/agent.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/clean.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/domain.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/entities.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/injector_prompt.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/rule_based_agent.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/scenarios.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/tools.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/trap.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/feedback.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/generic_world.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/injection.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/injector.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/llm.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/playbook.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/reachability.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/runner.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/scenario.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/scenarios/__init__.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/scenarios/clean.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/scenarios/decoy.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/scenarios/registry.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/scenarios/split.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/scenarios/trap.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/summarize.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/termui.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/tools.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/trajectory.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/triage.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/validate_scenarios.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/world.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/dependency_links.txt +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/entry_points.txt +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/requires.txt +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/top_level.txt +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/setup.cfg +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_access_control_agent.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_access_control_domain.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_access_control_rule_based_agent.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_access_control_scenarios.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_access_control_tools.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_agreement.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_browser_agent.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_classifier.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_cli.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_complex_access_control_agent.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_diff.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_domain.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_failures.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_feedback.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_generic_world.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_injection.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_injector.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_llm.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_package_api.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_playbook.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_quickstart.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_reachability.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_registry.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_report.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_rule_based_agent.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_runner.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_scenarios.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_target_agent.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_termui.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_tools.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_trajectory.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_triage.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_validate_scenarios.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_web_browsing_domain.py +0 -0
- {agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_world.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentprobe-testing
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Adaptive chaos-testing for LLM agents: a live model-driven Injector that reads an agent's real trajectory and decides where to break something, instead of scripting perturbations in advance.
|
|
5
5
|
License: Business Source License 1.1
|
|
6
6
|
|
|
@@ -60,7 +60,7 @@ from agentprobe.scenario import CommitPattern, FactPattern, GoalSpec, Scenario
|
|
|
60
60
|
from agentprobe.scenarios.registry import ALL_SCENARIOS as TICKET_SCENARIOS
|
|
61
61
|
from agentprobe.scenarios.registry import BY_ID as TICKET_SCENARIOS_BY_ID
|
|
62
62
|
|
|
63
|
-
__version__ = "1.
|
|
63
|
+
__version__ = "1.4.0"
|
|
64
64
|
|
|
65
65
|
__all__ = [
|
|
66
66
|
"Agent",
|
|
@@ -118,6 +118,20 @@ def rank(issues: list[dict], overrides: Optional[dict[str, str]] = None) -> list
|
|
|
118
118
|
return ranked
|
|
119
119
|
|
|
120
120
|
|
|
121
|
+
def clean_overrides(raw: object) -> dict[str, str]:
|
|
122
|
+
"""Keep only well-formed {FAILURE_KIND: "High"|"Medium"|"Low"} entries from
|
|
123
|
+
whatever a caller hands in (the hosted runner gets this from the website's
|
|
124
|
+
saved edits, over the wire), so a malformed or hostile value can never
|
|
125
|
+
reach a report."""
|
|
126
|
+
if not isinstance(raw, dict):
|
|
127
|
+
return {}
|
|
128
|
+
return {
|
|
129
|
+
k: v
|
|
130
|
+
for k, v in raw.items()
|
|
131
|
+
if isinstance(k, str) and isinstance(v, str) and v in TIERS and 1 <= len(k) <= 40 and k.replace("_", "").isalnum() and k.upper() == k
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
121
135
|
def encode(issues: list[dict]) -> str:
|
|
122
136
|
"""ASCII-only JSON, base64'd, so it can sit inside an HTML comment with no
|
|
123
137
|
escaping concerns. The website reads it back to re-rank a stored report
|
|
@@ -17,7 +17,8 @@ from typing import Any, Callable, Optional
|
|
|
17
17
|
from agentprobe.agents.base import Agent, AgentAction
|
|
18
18
|
from agentprobe.domain import Domain, TICKET_DOMAIN
|
|
19
19
|
from agentprobe.injection import injection_was_triggered
|
|
20
|
-
from agentprobe.injector import Injector, NullInjector, RecordingInjector, serialize_armed_injection
|
|
20
|
+
from agentprobe.injector import Injector, ModelInjector, NullInjector, RecordingInjector, ReplayInjector, serialize_armed_injection
|
|
21
|
+
from agentprobe.priority import clean_overrides
|
|
21
22
|
from agentprobe.report import InjectionRecord, Report
|
|
22
23
|
from agentprobe.runner import run_recovery, run_robustness_pair
|
|
23
24
|
from agentprobe.scenario import Scenario
|
|
@@ -162,6 +163,7 @@ def quick_test(
|
|
|
162
163
|
domain_key: Optional[str] = None,
|
|
163
164
|
record_injections: bool = False,
|
|
164
165
|
summarize_failures: bool = False,
|
|
166
|
+
priority_overrides: Optional[dict] = None,
|
|
165
167
|
) -> Report:
|
|
166
168
|
"""Run one scenario and get back a ready Report -- the one-call version
|
|
167
169
|
of run_robustness_pair()/run_recovery() + Report(). `agent` can be a
|
|
@@ -184,6 +186,7 @@ def quick_test(
|
|
|
184
186
|
injector = injector if injector is not None else NullInjector()
|
|
185
187
|
target_factory = agent if _looks_like_factory(agent) else wrap_agent(agent)
|
|
186
188
|
resolved_injector_model = injector_model if injector_model is not None else type(injector).__name__
|
|
189
|
+
injection_mode = _injection_mode(injector)
|
|
187
190
|
|
|
188
191
|
recording: Optional[RecordingInjector] = None
|
|
189
192
|
if record_injections:
|
|
@@ -214,6 +217,8 @@ def quick_test(
|
|
|
214
217
|
chaos_trajectories=[chaos],
|
|
215
218
|
injection_records=injection_records,
|
|
216
219
|
injector_cost_usd=_injector_cost_usd(injector),
|
|
220
|
+
priority_overrides=clean_overrides(priority_overrides),
|
|
221
|
+
injection_mode=injection_mode,
|
|
217
222
|
)
|
|
218
223
|
if recording is not None:
|
|
219
224
|
# Stashed here (not a Report field -- Report is a general-purpose
|
|
@@ -249,6 +254,7 @@ def quick_test_all(
|
|
|
249
254
|
record_injections: bool = False,
|
|
250
255
|
max_duration_seconds: Optional[float] = None,
|
|
251
256
|
summarize_failures: bool = False,
|
|
257
|
+
priority_overrides: Optional[dict] = None,
|
|
252
258
|
) -> Report:
|
|
253
259
|
"""Run every scenario in `scenarios` (a dict[str, Scenario] -- e.g.
|
|
254
260
|
TICKET_SCENARIOS or a FetchedDomain's .scenarios -- or a plain list of
|
|
@@ -296,6 +302,7 @@ def quick_test_all(
|
|
|
296
302
|
all_records: list[InjectionRecord] = []
|
|
297
303
|
total_injector_cost = 0.0
|
|
298
304
|
combined_injector_model: Optional[str] = None
|
|
305
|
+
combined_injection_mode: Optional[str] = None
|
|
299
306
|
start = time.monotonic()
|
|
300
307
|
scenarios_run = 0
|
|
301
308
|
|
|
@@ -307,6 +314,7 @@ def quick_test_all(
|
|
|
307
314
|
resolved_injector_model = injector_model if injector_model is not None else type(injector).__name__
|
|
308
315
|
if combined_injector_model is None:
|
|
309
316
|
combined_injector_model = resolved_injector_model
|
|
317
|
+
combined_injection_mode = _injection_mode(injector)
|
|
310
318
|
|
|
311
319
|
recording: Optional[RecordingInjector] = None
|
|
312
320
|
if record_injections:
|
|
@@ -352,6 +360,8 @@ def quick_test_all(
|
|
|
352
360
|
chaos_trajectories=all_chaos,
|
|
353
361
|
injection_records=all_records,
|
|
354
362
|
injector_cost_usd=total_injector_cost,
|
|
363
|
+
priority_overrides=clean_overrides(priority_overrides),
|
|
364
|
+
injection_mode=combined_injection_mode,
|
|
355
365
|
)
|
|
356
366
|
report._scenarios_run = len(all_chaos)
|
|
357
367
|
if summarize_failures:
|
|
@@ -391,6 +401,21 @@ def assert_passes(report: Report, save_html_on_failure: Optional[str] = None) ->
|
|
|
391
401
|
raise AssertionError("agentprobe: " + "; ".join(failures))
|
|
392
402
|
|
|
393
403
|
|
|
404
|
+
def _injection_mode(injector: Injector) -> Optional[str]:
|
|
405
|
+
"""How this run's failures were chosen, for labeling its report: an
|
|
406
|
+
adaptive ModelInjector decides afresh each time ("exploration" -- finds new
|
|
407
|
+
weaknesses, but two runs aren't directly comparable), a ReplayInjector
|
|
408
|
+
re-fires a recorded sequence ("pinned" -- the same failures every time, so
|
|
409
|
+
scores compare). Anything else (none, hardcoded) isn't labeled. Looks
|
|
410
|
+
through a RecordingInjector's wrapping."""
|
|
411
|
+
inner = getattr(injector, "_inner", injector)
|
|
412
|
+
if isinstance(inner, ReplayInjector):
|
|
413
|
+
return "pinned"
|
|
414
|
+
if isinstance(inner, ModelInjector):
|
|
415
|
+
return "exploration"
|
|
416
|
+
return None
|
|
417
|
+
|
|
418
|
+
|
|
394
419
|
def _injector_cost_usd(injector: Injector) -> float:
|
|
395
420
|
"""ModelInjector is the only injector type with real API cost (none/
|
|
396
421
|
hardcoded/replay never call a model) -- same logic as cli.py's version,
|
|
@@ -175,6 +175,7 @@ def upload_run(
|
|
|
175
175
|
# caller's explicit consent to pool them, anonymously, into
|
|
176
176
|
# aggregate benchmarks; False (the default) keeps them private.
|
|
177
177
|
"findings": report.findings(),
|
|
178
|
+
"injection_mode": report.injection_mode,
|
|
178
179
|
"share_anonymized": bool(share_anonymized),
|
|
179
180
|
}
|
|
180
181
|
if recorded_injections is None:
|
|
@@ -306,6 +306,11 @@ class Report:
|
|
|
306
306
|
holds Trajectories/InjectionRecords, not the Injector instances
|
|
307
307
|
themselves. 0.0 (and omitted from output) for none/hardcoded/replay
|
|
308
308
|
injectors, which never call a model at all."""
|
|
309
|
+
injection_mode: Optional[str] = None
|
|
310
|
+
"""How this run's failures were chosen: "exploration" (an adaptive injector
|
|
311
|
+
decided them -- finds new weaknesses, but two runs aren't directly
|
|
312
|
+
comparable) or "pinned" (a recorded sequence was replayed -- the same
|
|
313
|
+
failures every time, so scores compare). None when unknown/unlabeled."""
|
|
309
314
|
priority_overrides: dict[str, str] = field(default_factory=dict)
|
|
310
315
|
"""Tiers (High/Medium/Low) the reader has set for particular failure kinds,
|
|
311
316
|
e.g. {"FALSE_SUCCESS": "High"}. They replace the automatic estimate in
|
|
@@ -412,6 +417,13 @@ class Report:
|
|
|
412
417
|
)
|
|
413
418
|
return out
|
|
414
419
|
|
|
420
|
+
def injection_mode_label(self) -> Optional[str]:
|
|
421
|
+
"""Plain-language form of injection_mode for report headers."""
|
|
422
|
+
return {
|
|
423
|
+
"exploration": "adaptive failures (exploration: not directly comparable with other runs)",
|
|
424
|
+
"pinned": "replayed failures (pinned: comparable with other replays of the same recording)",
|
|
425
|
+
}.get(self.injection_mode or "")
|
|
426
|
+
|
|
415
427
|
def priorities(self) -> list[dict]:
|
|
416
428
|
"""The run's issues ranked highest to lowest priority, with any
|
|
417
429
|
`priority_overrides` applied. See priority.py for how it's scored."""
|
|
@@ -475,7 +487,7 @@ class Report:
|
|
|
475
487
|
lines = [
|
|
476
488
|
blue(
|
|
477
489
|
f"scenarios: {c['n']} mode: {self.mode} injector: {self.injector_model} "
|
|
478
|
-
f"target: {self.target_model}"
|
|
490
|
+
f"target: {self.target_model}" + (f" failures: {self.injection_mode_label()}" if self.injection_mode_label() else "")
|
|
479
491
|
),
|
|
480
492
|
]
|
|
481
493
|
if c["clean_passed"] is not None:
|
|
@@ -811,7 +823,7 @@ class Report:
|
|
|
811
823
|
<div class="page">
|
|
812
824
|
<header>
|
|
813
825
|
<div class="title-row"><span class="health-dot health-dot--{health}"></span><h1>AgentProbe report</h1></div>
|
|
814
|
-
<div class="subtitle">mode: {esc(self.mode)} · injector: {esc(self.injector_model)} · target: {esc(self.target_model)} · {esc(c['n'])} scenarios</div>
|
|
826
|
+
<div class="subtitle">mode: {esc(self.mode)} · injector: {esc(self.injector_model)} · target: {esc(self.target_model)} · {esc(c['n'])} scenarios{(' · ' + esc(self.injection_mode_label())) if self.injection_mode_label() else ''}</div>
|
|
815
827
|
</header>
|
|
816
828
|
{injector_error_banner}
|
|
817
829
|
<div class="stats">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentprobe-testing
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Adaptive chaos-testing for LLM agents: a live model-driven Injector that reads an agent's real trajectory and decides where to break something, instead of scripting perturbations in advance.
|
|
5
5
|
License: Business Source License 1.1
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agentprobe-testing"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.4.0"
|
|
4
4
|
description = "Adaptive chaos-testing for LLM agents: a live model-driven Injector that reads an agent's real trajectory and decides where to break something, instead of scripting perturbations in advance."
|
|
5
5
|
requires-python = ">=3.11"
|
|
6
6
|
dependencies = ["anthropic>=1.0.0", "python-dotenv>=1.0.0"]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Labeling a run exploration (adaptive injector) or pinned (replayed recording)."""
|
|
2
|
+
|
|
3
|
+
from agentprobe.agents.scripted import ScriptedAgent
|
|
4
|
+
from agentprobe.injection import ArmedInjection, Injection, InjectionKind, ResponseSignature, Trigger
|
|
5
|
+
from agentprobe.injector import HardcodedToolErrorInjector, ModelInjector, NullInjector, RecordingInjector, ReplayInjector, serialize_armed_injection, deserialize_armed_injection
|
|
6
|
+
from agentprobe.quickstart import _injection_mode, quick_test, quick_test_all
|
|
7
|
+
from agentprobe.registry import upload_run
|
|
8
|
+
from agentprobe.scenarios.registry import BY_ID
|
|
9
|
+
|
|
10
|
+
AGENT = lambda: ScriptedAgent(script=[("get_ticket", {"id": "T-1001"})])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _armed(step=1):
|
|
14
|
+
return ArmedInjection(
|
|
15
|
+
injection=Injection(kind=InjectionKind.TOOL_ERROR, payload={"tool_name": "close_ticket", "count": 1}, intent="i",
|
|
16
|
+
expected_signature=ResponseSignature("s"), rationale="r"),
|
|
17
|
+
trigger=Trigger.on_next_action(),
|
|
18
|
+
armed_at_step=step,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_injection_mode_reads_the_injector_type_through_a_recording_wrapper():
|
|
23
|
+
adaptive = ModelInjector.__new__(ModelInjector) # only its type matters here; no client needed
|
|
24
|
+
replay = ReplayInjector([_armed()])
|
|
25
|
+
assert _injection_mode(adaptive) == "exploration"
|
|
26
|
+
assert _injection_mode(replay) == "pinned"
|
|
27
|
+
assert _injection_mode(RecordingInjector(adaptive)) == "exploration"
|
|
28
|
+
assert _injection_mode(RecordingInjector(replay)) == "pinned"
|
|
29
|
+
assert _injection_mode(NullInjector()) is None
|
|
30
|
+
assert _injection_mode(HardcodedToolErrorInjector()) is None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_a_replayed_run_is_labeled_pinned_in_the_report():
|
|
34
|
+
report = quick_test(BY_ID["clean-1"], AGENT, injector=ReplayInjector([]), mode="recovery")
|
|
35
|
+
assert report.injection_mode == "pinned"
|
|
36
|
+
assert "replayed failures (pinned: comparable with other replays of the same recording)" in report.render_html()
|
|
37
|
+
assert "failures: replayed failures (pinned" in report.render()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_recording_a_replay_still_labels_it_pinned():
|
|
41
|
+
report = quick_test(BY_ID["clean-1"], AGENT, injector=ReplayInjector([]), mode="recovery", record_injections=True)
|
|
42
|
+
assert report.injection_mode == "pinned"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_an_unlabeled_injector_adds_no_mode_text():
|
|
46
|
+
report = quick_test(BY_ID["clean-1"], AGENT, injector=NullInjector(), mode="recovery")
|
|
47
|
+
assert report.injection_mode is None and report.injection_mode_label() is None
|
|
48
|
+
assert "pinned" not in report.render_html() and "exploration" not in report.render_html()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_the_exploration_label_says_runs_are_not_directly_comparable():
|
|
52
|
+
from agentprobe.report import Report
|
|
53
|
+
|
|
54
|
+
r = Report("robustness", "m", "t", [], [], injection_mode="exploration")
|
|
55
|
+
assert "adaptive failures (exploration: not directly comparable with other runs)" in r.render_html()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_quick_test_all_labels_the_combined_report_from_its_injectors():
|
|
59
|
+
scenarios = {k: BY_ID[k] for k in ("clean-1", "clean-2")}
|
|
60
|
+
report = quick_test_all(scenarios, AGENT, injector_factory=lambda: ReplayInjector([]), mode="recovery")
|
|
61
|
+
assert report.injection_mode == "pinned"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_upload_run_sends_the_mode_so_the_website_can_label_it():
|
|
65
|
+
sent = {}
|
|
66
|
+
report = quick_test(BY_ID["clean-1"], AGENT, injector=ReplayInjector([]), mode="recovery")
|
|
67
|
+
upload_run(report, api_key="k", poster=lambda url, body, key: sent.update(body))
|
|
68
|
+
assert sent["injection_mode"] == "pinned"
|
|
69
|
+
report2 = quick_test(BY_ID["clean-1"], AGENT, injector=NullInjector(), mode="recovery")
|
|
70
|
+
upload_run(report2, api_key="k", poster=lambda url, body, key: sent.update(body))
|
|
71
|
+
assert sent["injection_mode"] is None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_a_serialized_recording_survives_the_round_trip_the_website_relies_on():
|
|
75
|
+
# The website stores serialize_armed_injection() dicts and the runner rebuilds them with
|
|
76
|
+
# deserialize_armed_injection() before replaying -- this is the contract between the two.
|
|
77
|
+
original = [_armed(2), _armed(5)]
|
|
78
|
+
rebuilt = ReplayInjector([deserialize_armed_injection(serialize_armed_injection(a)) for a in original])
|
|
79
|
+
assert sorted(rebuilt._by_step) == [2, 5]
|
|
80
|
+
assert rebuilt._by_step[2].injection.kind == InjectionKind.TOOL_ERROR
|
|
@@ -139,3 +139,52 @@ def test_html_stays_script_free_and_the_text_report_lists_priorities():
|
|
|
139
139
|
out = r.render()
|
|
140
140
|
assert "priorities (automated estimate -- review before acting):" in out
|
|
141
141
|
assert "1. [High] Trusts a tool that did nothing -- 1 scenario(s)" in out
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# --- overrides flowing in from outside (the hosted runner / Temporal worker) ---
|
|
145
|
+
|
|
146
|
+
def test_clean_overrides_keeps_only_well_formed_entries():
|
|
147
|
+
from agentprobe.priority import clean_overrides
|
|
148
|
+
|
|
149
|
+
assert clean_overrides({"FALSE_SUCCESS": "High", "STALE_READ": "Low"}) == {"FALSE_SUCCESS": "High", "STALE_READ": "Low"}
|
|
150
|
+
dirty = {"false_success": "High", "STALE READ": "Low", "TOOL_ERROR": "Urgent", "DISTRACTOR": 3, 5: "High", "X" * 41: "High", "": "High", "OK_KIND": "Medium"}
|
|
151
|
+
assert clean_overrides(dirty) == {"OK_KIND": "Medium"}
|
|
152
|
+
for junk in (None, "High", ["High"], 7):
|
|
153
|
+
assert clean_overrides(junk) == {}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_quick_test_ranks_with_the_overrides_it_is_given():
|
|
157
|
+
from agentprobe.agents.scripted import ScriptedAgent
|
|
158
|
+
from agentprobe.injector import NullInjector
|
|
159
|
+
from agentprobe.quickstart import quick_test
|
|
160
|
+
from agentprobe.scenarios.registry import BY_ID
|
|
161
|
+
|
|
162
|
+
agent = lambda: ScriptedAgent(script=[("get_ticket", {"id": "T-1001"})]) # never finishes -> the chaos run fails
|
|
163
|
+
report = quick_test(BY_ID["clean-1"], agent, injector=NullInjector(), mode="recovery", priority_overrides={"UNATTRIBUTED": "High", "BAD": "nope", "x": "Low"})
|
|
164
|
+
assert report.priority_overrides == {"UNATTRIBUTED": "High"} # junk dropped
|
|
165
|
+
ranked = report.priorities()
|
|
166
|
+
assert [(r["kind"], r["tier"], r["edited"]) for r in ranked] == [("UNATTRIBUTED", "High", True)]
|
|
167
|
+
assert "priority--high'>High</span> <span class='muted'>(edited)" in report.render_html()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_quick_test_without_overrides_ranks_automatically():
|
|
171
|
+
from agentprobe.agents.scripted import ScriptedAgent
|
|
172
|
+
from agentprobe.injector import NullInjector
|
|
173
|
+
from agentprobe.quickstart import quick_test
|
|
174
|
+
from agentprobe.scenarios.registry import BY_ID
|
|
175
|
+
|
|
176
|
+
report = quick_test(BY_ID["clean-1"], lambda: ScriptedAgent(script=[("get_ticket", {"id": "T-1001"})]), injector=NullInjector(), mode="recovery")
|
|
177
|
+
assert report.priority_overrides == {}
|
|
178
|
+
assert [(r["tier"], r["edited"]) for r in report.priorities()] == [("Medium", False)]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_quick_test_all_carries_the_overrides_onto_the_combined_report():
|
|
182
|
+
from agentprobe.agents.scripted import ScriptedAgent
|
|
183
|
+
from agentprobe.injector import NullInjector
|
|
184
|
+
from agentprobe.quickstart import quick_test_all
|
|
185
|
+
from agentprobe.scenarios.registry import BY_ID
|
|
186
|
+
|
|
187
|
+
scenarios = {k: BY_ID[k] for k in ("clean-1", "clean-2")}
|
|
188
|
+
report = quick_test_all(scenarios, lambda: ScriptedAgent(script=[("get_ticket", {"id": "T-1001"})]), injector_factory=NullInjector, mode="recovery", priority_overrides={"UNATTRIBUTED": "Low"})
|
|
189
|
+
assert report.priority_overrides == {"UNATTRIBUTED": "Low"}
|
|
190
|
+
assert all(r["tier"] == "Low" and r["edited"] for r in report.priorities())
|
|
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
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/__init__.py
RENAMED
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/agent.py
RENAMED
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/clean.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/decoy.py
RENAMED
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/domain.py
RENAMED
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/entities.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/scenarios.py
RENAMED
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/split.py
RENAMED
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/tools.py
RENAMED
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/access_control/trap.py
RENAMED
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/__init__.py
RENAMED
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/agent.py
RENAMED
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/clean.py
RENAMED
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/domain.py
RENAMED
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/entities.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/scenarios.py
RENAMED
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/tools.py
RENAMED
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe/domains/web_browsing/trap.py
RENAMED
|
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
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/requires.txt
RENAMED
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/agentprobe_testing.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_access_control_rule_based_agent.py
RENAMED
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_access_control_scenarios.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentprobe_testing-1.2.0 → agentprobe_testing-1.4.0}/tests/test_complex_access_control_agent.py
RENAMED
|
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
|