agentprobe-testing 0.5.0__tar.gz → 0.5.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.
Files changed (92) hide show
  1. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/PKG-INFO +1 -1
  2. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/README.md +19 -0
  3. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/__init__.py +3 -2
  4. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/quickstart.py +60 -0
  5. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe_testing.egg-info/PKG-INFO +1 -1
  6. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/pyproject.toml +1 -1
  7. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_quickstart.py +76 -1
  8. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/LICENSE +0 -0
  9. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/agents/__init__.py +0 -0
  10. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/agents/base.py +0 -0
  11. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/agents/rule_based.py +0 -0
  12. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/agents/scripted.py +0 -0
  13. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/agents/target_agent.py +0 -0
  14. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/agreement.py +0 -0
  15. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/classifier.py +0 -0
  16. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/cli.py +0 -0
  17. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/diff.py +0 -0
  18. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/domain.py +0 -0
  19. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/domains/__init__.py +0 -0
  20. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/domains/access_control/__init__.py +0 -0
  21. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/domains/access_control/agent.py +0 -0
  22. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/domains/access_control/clean.py +0 -0
  23. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/domains/access_control/complex_agent.py +0 -0
  24. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/domains/access_control/decoy.py +0 -0
  25. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/domains/access_control/domain.py +0 -0
  26. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/domains/access_control/entities.py +0 -0
  27. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/domains/access_control/injector_prompt.py +0 -0
  28. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/domains/access_control/rule_based_agent.py +0 -0
  29. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/domains/access_control/scenarios.py +0 -0
  30. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/domains/access_control/split.py +0 -0
  31. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/domains/access_control/tools.py +0 -0
  32. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/domains/access_control/trap.py +0 -0
  33. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/feedback.py +0 -0
  34. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/generic_world.py +0 -0
  35. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/injection.py +0 -0
  36. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/injector.py +0 -0
  37. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/llm.py +0 -0
  38. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/playbook.py +0 -0
  39. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/reachability.py +0 -0
  40. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/registry.py +0 -0
  41. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/report.py +0 -0
  42. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/runner.py +0 -0
  43. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/scenario.py +0 -0
  44. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/scenarios/__init__.py +0 -0
  45. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/scenarios/clean.py +0 -0
  46. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/scenarios/decoy.py +0 -0
  47. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/scenarios/registry.py +0 -0
  48. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/scenarios/split.py +0 -0
  49. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/scenarios/trap.py +0 -0
  50. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/termui.py +0 -0
  51. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/tools.py +0 -0
  52. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/trajectory.py +0 -0
  53. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/triage.py +0 -0
  54. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/validate_scenarios.py +0 -0
  55. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe/world.py +0 -0
  56. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe_testing.egg-info/SOURCES.txt +0 -0
  57. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe_testing.egg-info/dependency_links.txt +0 -0
  58. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe_testing.egg-info/entry_points.txt +0 -0
  59. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe_testing.egg-info/requires.txt +0 -0
  60. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/agentprobe_testing.egg-info/top_level.txt +0 -0
  61. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/setup.cfg +0 -0
  62. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_access_control_agent.py +0 -0
  63. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_access_control_domain.py +0 -0
  64. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_access_control_rule_based_agent.py +0 -0
  65. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_access_control_scenarios.py +0 -0
  66. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_access_control_tools.py +0 -0
  67. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_agreement.py +0 -0
  68. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_classifier.py +0 -0
  69. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_cli.py +0 -0
  70. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_complex_access_control_agent.py +0 -0
  71. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_diff.py +0 -0
  72. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_domain.py +0 -0
  73. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_feedback.py +0 -0
  74. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_generic_world.py +0 -0
  75. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_injection.py +0 -0
  76. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_injector.py +0 -0
  77. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_llm.py +0 -0
  78. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_package_api.py +0 -0
  79. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_playbook.py +0 -0
  80. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_reachability.py +0 -0
  81. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_registry.py +0 -0
  82. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_report.py +0 -0
  83. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_rule_based_agent.py +0 -0
  84. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_runner.py +0 -0
  85. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_scenarios.py +0 -0
  86. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_target_agent.py +0 -0
  87. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_termui.py +0 -0
  88. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_tools.py +0 -0
  89. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_trajectory.py +0 -0
  90. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_triage.py +0 -0
  91. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_validate_scenarios.py +0 -0
  92. {agentprobe_testing-0.5.0 → agentprobe_testing-0.5.1}/tests/test_world.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentprobe-testing
3
- Version: 0.5.0
3
+ Version: 0.5.1
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
 
@@ -18,6 +18,19 @@ adversarial content disguised as ordinary data (`PROMPT_INJECTION`).
18
18
 
19
19
  ## Quickstart
20
20
 
21
+ Using this in your own project (no need to clone this repo):
22
+
23
+ ```bash
24
+ pip install agentprobe-testing
25
+ ```
26
+
27
+ `import agentprobe as ap` afterward, same as everywhere else in this
28
+ README — the PyPI *distribution* name is `agentprobe-testing`, but the
29
+ Python package you import is still plain `agentprobe`.
30
+
31
+ To run the examples in this repo (or work on the library itself), clone
32
+ it and install from source instead:
33
+
21
34
  ```bash
22
35
  pip install -e .
23
36
  python examples/free_zero_cost_demo.py # no API key needed, no network calls at all
@@ -105,6 +118,12 @@ surface. The `access_control` domain lives at
105
118
  top level, to keep this surface to the one domain most people reach for
106
119
  first.
107
120
 
121
+ Testing your own agent instead of the built-in `TargetAgent`? Pass any
122
+ plain `(task, history) -> AgentAction` function as `agent` — no subclass
123
+ needed, `quick_test`/`wrap_agent` handle the wiring, and if your agent
124
+ already runs as a service you can POST to, `ap.http_agent(url)` builds
125
+ that function for you (see [USAGE.md](USAGE.md)).
126
+
108
127
  Beyond one-scenario `quick_test()`, see [USAGE.md](USAGE.md) for: running
109
128
  a whole domain at once (`quick_test_all`), a one-line pytest integration
110
129
  (`assert_passes`), catching regressions between uploaded runs
@@ -44,7 +44,7 @@ from agentprobe.injector import (
44
44
  RecordingInjector,
45
45
  ReplayInjector,
46
46
  )
47
- from agentprobe.quickstart import assert_passes, quick_test, quick_test_all, wrap_agent
47
+ from agentprobe.quickstart import assert_passes, http_agent, quick_test, quick_test_all, wrap_agent
48
48
  from agentprobe.registry import (
49
49
  FetchedDomain,
50
50
  RegressionResult,
@@ -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__ = "0.5.0"
63
+ __version__ = "0.5.1"
64
64
 
65
65
  __all__ = [
66
66
  "Agent",
@@ -93,6 +93,7 @@ __all__ = [
93
93
  "check_regression",
94
94
  "fetch_domain",
95
95
  "get_latest_run",
96
+ "http_agent",
96
97
  "injection_was_triggered",
97
98
  "quick_test",
98
99
  "quick_test_all",
@@ -9,6 +9,8 @@ what to do next, test it against one scenario."
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import json
13
+ import urllib.request
12
14
  from typing import Any, Callable, Optional
13
15
 
14
16
  from agentprobe.agents.base import Agent, AgentAction
@@ -57,6 +59,64 @@ def wrap_agent(decide_fn: DecideFn) -> Callable[[], Agent]:
57
59
  return lambda: _FunctionAgent(decide_fn)
58
60
 
59
61
 
62
+ HttpRequester = Callable[[str, dict, dict], dict] # (url, body, headers) -> response dict
63
+
64
+
65
+ def _default_http_requester(url: str, body: dict, headers: dict) -> dict:
66
+ data = json.dumps(body).encode("utf-8")
67
+ request = urllib.request.Request(
68
+ url,
69
+ data=data,
70
+ method="POST",
71
+ headers={"Content-Type": "application/json", "User-Agent": "agentprobe-client/0.5.0", **headers},
72
+ )
73
+ with urllib.request.urlopen(request, timeout=30) as resp: # noqa: S310 -- caller-supplied url, same trust model as any target agent's own API
74
+ return json.loads(resp.read().decode("utf-8"))
75
+
76
+
77
+ def http_agent(
78
+ url: str,
79
+ headers: Optional[dict] = None,
80
+ requester: Optional[HttpRequester] = None,
81
+ ) -> Callable[[], Agent]:
82
+ """Adapt a hosted HTTP agent into a target_factory for quick_test()/
83
+ run_robustness_pair() -- for when your real agent already runs as a
84
+ service you can POST to, instead of a Python function/object living in
85
+ this same process. No custom decide_fn to write: this builds one for
86
+ you and wraps it exactly like wrap_agent() does.
87
+
88
+ On every step, POSTs `{"task": <str>, "history": [...]}` as JSON to
89
+ `url` -- `history` is the same shape wrap_agent's decide_fn receives, a
90
+ list of `{"tool_name", "tool_args", "result", "ok"}` dicts covering
91
+ everything observed so far this run. Expects back JSON shaped like
92
+ either:
93
+
94
+ {"tool_name": "...", "tool_args": {...}} -- call this tool next
95
+ {"final_answer": "..."} -- done, this is the reply
96
+
97
+ Raises ValueError if a response matches neither shape, rather than
98
+ guessing what was meant. Pass `headers` (e.g.
99
+ `{"Authorization": "Bearer ..."}`) for an endpoint that needs auth, and
100
+ `requester` in tests instead of hitting the real network -- see
101
+ tests/test_quickstart.py.
102
+ """
103
+ requester = requester if requester is not None else _default_http_requester
104
+ resolved_headers = headers or {}
105
+
106
+ def decide_fn(task: str, history: list[dict[str, Any]]) -> AgentAction:
107
+ response = requester(url, {"task": task, "history": history}, resolved_headers)
108
+ if "final_answer" in response:
109
+ return AgentAction(kind="final_answer", text=response["final_answer"])
110
+ if "tool_name" in response and "tool_args" in response:
111
+ return AgentAction(kind="tool_call", tool_name=response["tool_name"], tool_args=response["tool_args"])
112
+ raise ValueError(
113
+ f"http_agent: response from {url} must include either 'final_answer' or "
114
+ f"both 'tool_name' and 'tool_args', got keys {sorted(response.keys())}"
115
+ )
116
+
117
+ return wrap_agent(decide_fn)
118
+
119
+
60
120
  def quick_test(
61
121
  scenario: Scenario,
62
122
  agent: Callable[[], Agent] | DecideFn,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentprobe-testing
3
- Version: 0.5.0
3
+ Version: 0.5.1
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 = "0.5.0"
3
+ version = "0.5.1"
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"]
@@ -3,7 +3,7 @@ import pytest
3
3
  from agentprobe.agents.base import AgentAction
4
4
  from agentprobe.agents.rule_based import RuleBasedAgent
5
5
  from agentprobe.injector import HardcodedToolErrorInjector, Injector, NullInjector
6
- from agentprobe.quickstart import assert_passes, quick_test, quick_test_all, wrap_agent
6
+ from agentprobe.quickstart import assert_passes, http_agent, quick_test, quick_test_all, wrap_agent
7
7
  from agentprobe.report import Report
8
8
  from agentprobe.scenarios.registry import BY_ID
9
9
 
@@ -52,6 +52,81 @@ def test_wrap_agent_gives_each_call_its_own_history_not_shared_across_instances(
52
52
  assert action.tool_name == "get_ticket" # fresh history, not polluted by `first`
53
53
 
54
54
 
55
+ def make_requester(responses):
56
+ """Returns each queued response in order, on every call -- the same
57
+ scripted-sequence pattern ScriptedAgent uses, since http_agent's whole
58
+ point is standing in for a real remote agent step by step."""
59
+ calls = []
60
+ responses = list(responses)
61
+
62
+ def requester(url, body, headers):
63
+ calls.append({"url": url, "body": body, "headers": headers})
64
+ return responses.pop(0)
65
+
66
+ requester.calls = calls
67
+ return requester
68
+
69
+
70
+ def test_http_agent_drives_a_full_run_from_scripted_json_responses():
71
+ # mode="recovery" -- a robustness-mode run calls the target_factory
72
+ # twice (clean then chaos), which would drain this same scripted
73
+ # response queue twice over; recovery mode's single trajectory keeps
74
+ # this test's response list simple and matched 1:1 to real steps.
75
+ scenario = BY_ID["clean-1"]
76
+ requester = make_requester(
77
+ [
78
+ {"tool_name": "get_ticket", "tool_args": {"id": "T-1001"}},
79
+ {"tool_name": "issue_refund", "tool_args": {"ticket_id": "T-1001", "amount": 49.99}},
80
+ {"tool_name": "close_ticket", "tool_args": {"ticket_id": "T-1001", "resolution": "refunded"}},
81
+ {"final_answer": "done"},
82
+ ]
83
+ )
84
+
85
+ report = quick_test(scenario, http_agent("https://example.com/agent", requester=requester), injector=NullInjector(), mode="recovery")
86
+
87
+ assert report._compute()["chaos_passed"] == 1
88
+ assert requester.calls[0]["url"] == "https://example.com/agent"
89
+
90
+
91
+ def test_http_agent_sends_task_and_history_as_the_request_body():
92
+ scenario = BY_ID["clean-1"]
93
+ requester = make_requester(
94
+ [
95
+ {"tool_name": "get_ticket", "tool_args": {"id": "T-1001"}},
96
+ {"final_answer": "done"},
97
+ ]
98
+ )
99
+
100
+ quick_test(scenario, http_agent("https://example.com/agent", requester=requester), injector=NullInjector(), mode="recovery")
101
+
102
+ first_call, second_call = requester.calls
103
+ assert first_call["body"]["task"] == "Handle ticket T-1001."
104
+ assert first_call["body"]["history"] == []
105
+ assert second_call["body"]["history"][0]["tool_name"] == "get_ticket"
106
+
107
+
108
+ def test_http_agent_passes_through_custom_headers():
109
+ scenario = BY_ID["clean-1"]
110
+ requester = make_requester([{"final_answer": "done"}])
111
+
112
+ quick_test(
113
+ scenario,
114
+ http_agent("https://example.com/agent", headers={"Authorization": "Bearer secret"}, requester=requester),
115
+ injector=NullInjector(),
116
+ mode="recovery",
117
+ )
118
+
119
+ assert requester.calls[0]["headers"] == {"Authorization": "Bearer secret"}
120
+
121
+
122
+ def test_http_agent_raises_a_clear_error_on_a_response_matching_neither_shape():
123
+ scenario = BY_ID["clean-1"]
124
+ requester = make_requester([{"something_else": "not a valid response"}])
125
+
126
+ with pytest.raises(ValueError, match="must include either 'final_answer' or"):
127
+ quick_test(scenario, http_agent("https://example.com/agent", requester=requester), injector=NullInjector(), mode="recovery")
128
+
129
+
55
130
  def test_quick_test_runs_a_plain_function_end_to_end_and_reports_pass():
56
131
  scenario = BY_ID["clean-1"]
57
132
  report = quick_test(scenario, _clean1_decide, injector=NullInjector())