entropy-ai 0.1.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.
- entropy_ai-0.1.0/PKG-INFO +34 -0
- entropy_ai-0.1.0/README.md +125 -0
- entropy_ai-0.1.0/entropy/__init__.py +52 -0
- entropy_ai-0.1.0/entropy/benchmarks/__init__.py +10 -0
- entropy_ai-0.1.0/entropy/benchmarks/bench.py +59 -0
- entropy_ai-0.1.0/entropy/chaos/__init__.py +7 -0
- entropy_ai-0.1.0/entropy/chaos/faults.py +101 -0
- entropy_ai-0.1.0/entropy/chaos/runner.py +40 -0
- entropy_ai-0.1.0/entropy/cli/__init__.py +6 -0
- entropy_ai-0.1.0/entropy/cli/__main__.py +6 -0
- entropy_ai-0.1.0/entropy/cli/main.py +332 -0
- entropy_ai-0.1.0/entropy/core/__init__.py +20 -0
- entropy_ai-0.1.0/entropy/core/exporters.py +104 -0
- entropy_ai-0.1.0/entropy/core/models.py +122 -0
- entropy_ai-0.1.0/entropy/core/suite.py +141 -0
- entropy_ai-0.1.0/entropy/core/tracing.py +86 -0
- entropy_ai-0.1.0/entropy/dashboard/__init__.py +6 -0
- entropy_ai-0.1.0/entropy/dashboard/app.py +123 -0
- entropy_ai-0.1.0/entropy/datasets/__init__.py +12 -0
- entropy_ai-0.1.0/entropy/datasets/cases.py +83 -0
- entropy_ai-0.1.0/entropy/datasets/loaders.py +66 -0
- entropy_ai-0.1.0/entropy/integrations/__init__.py +17 -0
- entropy_ai-0.1.0/entropy/integrations/autogen.py +31 -0
- entropy_ai-0.1.0/entropy/integrations/base.py +49 -0
- entropy_ai-0.1.0/entropy/integrations/crewai.py +27 -0
- entropy_ai-0.1.0/entropy/integrations/custom.py +28 -0
- entropy_ai-0.1.0/entropy/integrations/google_adk.py +32 -0
- entropy_ai-0.1.0/entropy/integrations/langchain.py +39 -0
- entropy_ai-0.1.0/entropy/integrations/langgraph.py +28 -0
- entropy_ai-0.1.0/entropy/integrations/mcp.py +30 -0
- entropy_ai-0.1.0/entropy/integrations/openai.py +28 -0
- entropy_ai-0.1.0/entropy/integrations/pydanticai.py +27 -0
- entropy_ai-0.1.0/entropy/integrations/wire.py +49 -0
- entropy_ai-0.1.0/entropy/metrics/__init__.py +25 -0
- entropy_ai-0.1.0/entropy/metrics/behavioral.py +77 -0
- entropy_ai-0.1.0/entropy/metrics/drift.py +61 -0
- entropy_ai-0.1.0/entropy/metrics/entropy_.py +84 -0
- entropy_ai-0.1.0/entropy/metrics/execution.py +90 -0
- entropy_ai-0.1.0/entropy/metrics/layers.py +34 -0
- entropy_ai-0.1.0/entropy/metrics/memory.py +57 -0
- entropy_ai-0.1.0/entropy/metrics/multiagent.py +52 -0
- entropy_ai-0.1.0/entropy/metrics/reasoning.py +59 -0
- entropy_ai-0.1.0/entropy/metrics/registry.py +186 -0
- entropy_ai-0.1.0/entropy/metrics/safety.py +64 -0
- entropy_ai-0.1.0/entropy/metrics/statistical.py +85 -0
- entropy_ai-0.1.0/entropy/metrics/tool.py +85 -0
- entropy_ai-0.1.0/entropy/observability/__init__.py +6 -0
- entropy_ai-0.1.0/entropy/observability/watch.py +102 -0
- entropy_ai-0.1.0/entropy/plugins/__init__.py +8 -0
- entropy_ai-0.1.0/entropy/plugins/manager.py +51 -0
- entropy_ai-0.1.0/entropy/py.typed +0 -0
- entropy_ai-0.1.0/entropy/regression/__init__.py +6 -0
- entropy_ai-0.1.0/entropy/regression/test.py +42 -0
- entropy_ai-0.1.0/entropy/reports/__init__.py +8 -0
- entropy_ai-0.1.0/entropy/reports/exporters.py +88 -0
- entropy_ai-0.1.0/entropy/simulation/__init__.py +13 -0
- entropy_ai-0.1.0/entropy/simulation/adversarial.py +42 -0
- entropy_ai-0.1.0/entropy/simulation/environment.py +52 -0
- entropy_ai-0.1.0/entropy/simulation/user.py +72 -0
- entropy_ai-0.1.0/entropy_ai.egg-info/PKG-INFO +34 -0
- entropy_ai-0.1.0/entropy_ai.egg-info/SOURCES.txt +71 -0
- entropy_ai-0.1.0/entropy_ai.egg-info/dependency_links.txt +1 -0
- entropy_ai-0.1.0/entropy_ai.egg-info/entry_points.txt +2 -0
- entropy_ai-0.1.0/entropy_ai.egg-info/requires.txt +39 -0
- entropy_ai-0.1.0/entropy_ai.egg-info/top_level.txt +1 -0
- entropy_ai-0.1.0/pyproject.toml +38 -0
- entropy_ai-0.1.0/setup.cfg +4 -0
- entropy_ai-0.1.0/tests/test_basic.py +71 -0
- entropy_ai-0.1.0/tests/test_langchain_adapter.py +30 -0
- entropy_ai-0.1.0/tests/test_phase1.py +130 -0
- entropy_ai-0.1.0/tests/test_phase3.py +100 -0
- entropy_ai-0.1.0/tests/test_phase4.py +92 -0
- entropy_ai-0.1.0/tests/test_vision_metrics.py +48 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: entropy-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Behavioral evaluation framework for non-deterministic AI agents
|
|
5
|
+
Author-email: Satyam Singh <satyamsingh7734@gmail.com>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Requires-Dist: pandas
|
|
8
|
+
Requires-Dist: matplotlib
|
|
9
|
+
Requires-Dist: jinja2
|
|
10
|
+
Requires-Dist: rich
|
|
11
|
+
Requires-Dist: pyyaml
|
|
12
|
+
Provides-Extra: pdf
|
|
13
|
+
Requires-Dist: reportlab; extra == "pdf"
|
|
14
|
+
Provides-Extra: jupyter
|
|
15
|
+
Requires-Dist: nbformat; extra == "jupyter"
|
|
16
|
+
Provides-Extra: hf
|
|
17
|
+
Requires-Dist: datasets; extra == "hf"
|
|
18
|
+
Provides-Extra: langchain
|
|
19
|
+
Requires-Dist: langchain; extra == "langchain"
|
|
20
|
+
Provides-Extra: langgraph
|
|
21
|
+
Requires-Dist: langgraph; extra == "langgraph"
|
|
22
|
+
Provides-Extra: openai
|
|
23
|
+
Requires-Dist: openai; extra == "openai"
|
|
24
|
+
Requires-Dist: openai-agents; extra == "openai"
|
|
25
|
+
Provides-Extra: crewai
|
|
26
|
+
Requires-Dist: crewai; extra == "crewai"
|
|
27
|
+
Provides-Extra: pydanticai
|
|
28
|
+
Requires-Dist: pydantic-ai; extra == "pydanticai"
|
|
29
|
+
Provides-Extra: autogen
|
|
30
|
+
Requires-Dist: autogen; extra == "autogen"
|
|
31
|
+
Provides-Extra: google-adk
|
|
32
|
+
Requires-Dist: google-adk; extra == "google-adk"
|
|
33
|
+
Provides-Extra: mcp
|
|
34
|
+
Requires-Dist: mcp; extra == "mcp"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# EntroPy
|
|
2
|
+
|
|
3
|
+
Behavioral evaluation framework for non-deterministic AI agents.
|
|
4
|
+
|
|
5
|
+
Measures what other frameworks miss: **uncertainty, drift, stochastic
|
|
6
|
+
behavior, and emergent failures** — via Monte Carlo evaluation over repeated
|
|
7
|
+
runs and entropy-based metrics. Framework-agnostic, local-first, zero vendor
|
|
8
|
+
lock-in.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install -e . # core (pandas, matplotlib, jinja2, rich, pyyaml)
|
|
14
|
+
pip install "entropy-ai[langchain]" # optional: a framework adapter
|
|
15
|
+
pip install "entropy-ai[pdf,jupyter]" # optional: PDF / Jupyter reports
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from entropy import Suite, Dataset, Case
|
|
22
|
+
|
|
23
|
+
def my_agent(inp): ...
|
|
24
|
+
# return an output, an AgentRun, or a dict with events
|
|
25
|
+
|
|
26
|
+
dataset = Dataset([Case(input="q1", expected="correct answer")])
|
|
27
|
+
results = Suite(seed=42).run(my_agent, dataset, trials=100)
|
|
28
|
+
print(results) # success_rate, behavioral_entropy, drift_score, ...
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The canonical output (spec §9 / §21) includes: `success_rate`, `reliability`,
|
|
32
|
+
`variance`, `robustness`, `entropy`, `confidence_interval`, `behavioral_entropy`,
|
|
33
|
+
`behavioral_variance`, `trajectory_entropy`, `trajectory_diversity`, `tool_reliability`,
|
|
34
|
+
`drift_score`, `goal_stability`, `recovery_score`, `emergent_behavior`, and more.
|
|
35
|
+
56 metrics total; `entropy.default_metrics()` lists the canonical set.
|
|
36
|
+
|
|
37
|
+
## Tracing
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from entropy import trace, event, instrument
|
|
41
|
+
with trace() as t:
|
|
42
|
+
event("action", "tool:search")
|
|
43
|
+
# or auto-wrap any agent:
|
|
44
|
+
wrapped = instrument(my_agent) # returns an AgentRun per call
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Custom metrics (plugin system)
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from entropy import metric
|
|
51
|
+
@metric("my_metric")
|
|
52
|
+
class MyMetric:
|
|
53
|
+
def compute(self, batch): ... # batch has outputs/events/successes/costs/...
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Simulation, Chaos, Observability
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from entropy import AdversarialSimulator, ChaosRunner, Watcher
|
|
60
|
+
adv = AdversarialSimulator(seed=0)
|
|
61
|
+
for v in adv.iterate("book a flight"):
|
|
62
|
+
print(v) # typo / injection / distraction / leak variants
|
|
63
|
+
|
|
64
|
+
ChaosRunner(seed=0).run(agent, dataset, ["api_fail", "timeout"]) # fault injection
|
|
65
|
+
Watcher(agent, dataset, trials=20).watch(rounds=10) # live drift + anomalies
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Framework adapters (lazy)
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from entropy import from_langchain, from_langgraph, from_openai, from_custom
|
|
72
|
+
runnable = from_langchain(my_langchain_agent) # maps framework runs -> AgentRun
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
All 9 adapters (LangChain, LangGraph, OpenAI Agents, CrewAI, PydanticAI,
|
|
76
|
+
AutoGen, Google ADK, MCP, custom) import their framework **lazily**, so
|
|
77
|
+
`import entropy` works with none of them installed.
|
|
78
|
+
|
|
79
|
+
## CLI
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
entropy init # scaffold a project
|
|
83
|
+
entropy run --agent m:a --dataset d.json --trials 100
|
|
84
|
+
entropy test --agent m:a --dataset d.json --baseline base.json [--update]
|
|
85
|
+
entropy benchmark --agents m:a m:b --dataset d.json
|
|
86
|
+
entropy chaos --agent m:a --dataset d.json --faults api_fail,timeout
|
|
87
|
+
entropy simulate --kind adversarial --text "book a flight"
|
|
88
|
+
entropy ui --agent m:a --dataset d.json --serve
|
|
89
|
+
entropy watch --agent m:a --dataset d.json --rounds 10
|
|
90
|
+
entropy report --results r.json --format html
|
|
91
|
+
entropy dataset --path d.json
|
|
92
|
+
entropy doctor # check optional deps
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Reports
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from entropy import export_report
|
|
99
|
+
export_report(results, "html", "report.html") # also: json, markdown, pdf, jupyter
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Layout
|
|
103
|
+
|
|
104
|
+
`core/` (models, tracing, exporters, suite) · `metrics/` (registry + all
|
|
105
|
+
categories) · `datasets/` · `simulation/` · `chaos/` · `regression/` ·
|
|
106
|
+
`benchmarks/` · `observability/` · `reports/` · `dashboard/` · `integrations/`
|
|
107
|
+
(adapters) · `cli/` · `plugins/`.
|
|
108
|
+
|
|
109
|
+
Tests: `python -m pytest tests/`.
|
|
110
|
+
|
|
111
|
+
## Documentation
|
|
112
|
+
|
|
113
|
+
The static documentation site lives in [`docs/`](docs/) — plain
|
|
114
|
+
HTML/CSS/JS with no build step, deployable to GitHub Pages.
|
|
115
|
+
|
|
116
|
+
- Source: `docs/index.html`, `docs/guide/*.html`, `docs/api.html`,
|
|
117
|
+
`docs/examples.html`, plus `docs/assets/` (CSS, JS, logo, search index).
|
|
118
|
+
- Local preview: serve the folder, e.g. `python -m http.server -d docs 8000`
|
|
119
|
+
then open `http://localhost:8000`.
|
|
120
|
+
- Deploy: push to `main`; the
|
|
121
|
+
[`.github/workflows/pages.yml`](.github/workflows/pages.yml) workflow
|
|
122
|
+
uploads `docs/` and publishes it via GitHub Pages. Alternatively, set
|
|
123
|
+
**Settings → Pages → Build and deployment → Source** to *Deploy from a
|
|
124
|
+
branch* and choose the `main` branch `/docs` folder.
|
|
125
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""EntroPy — behavioral evaluation for non-deterministic AI agents.
|
|
2
|
+
|
|
3
|
+
Measure uncertainty, drift, stochastic behavior, and emergent failures via
|
|
4
|
+
Monte Carlo evaluation and entropy-based metrics.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from .core import (
|
|
9
|
+
AgentRun, Event, Trace, Node, Edge,
|
|
10
|
+
ActionEvent, ReasoningEvent, ToolCallEvent, ObservationEvent,
|
|
11
|
+
MemoryReadEvent, MemoryWriteEvent, ErrorEvent, StateTransitionEvent,
|
|
12
|
+
instrument, trace, event, current_trace,
|
|
13
|
+
Suite, assert_stable,
|
|
14
|
+
)
|
|
15
|
+
from .metrics import metric, Metric, Batch, default_metrics, layers
|
|
16
|
+
from .datasets import (
|
|
17
|
+
Dataset, Case, GoldenCase, Scenario, BehaviorCase, FailureCase, AdversarialCase,
|
|
18
|
+
)
|
|
19
|
+
from .simulation import (
|
|
20
|
+
UserSimulator, ScriptedUserSimulator, RandomUserSimulator, LLMUserSimulator,
|
|
21
|
+
Environment, StatefulEnv, GridWorld, AdversarialSimulator,
|
|
22
|
+
)
|
|
23
|
+
from .chaos import ChaosRunner, inject as chaos_inject, available as chaos_available
|
|
24
|
+
from .observability import Watcher, render_metric_heatmap
|
|
25
|
+
from .dashboard import build_dashboard, serve as serve_dashboard
|
|
26
|
+
from .reports import export as export_report
|
|
27
|
+
from .integrations import (
|
|
28
|
+
adapter, find_adapter, list_adapters,
|
|
29
|
+
from_langchain, from_langgraph, from_openai, from_crewai,
|
|
30
|
+
from_pydanticai, from_autogen, from_google_adk, from_mcp, from_custom,
|
|
31
|
+
)
|
|
32
|
+
from .plugins import exporter, discover
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"Suite", "Dataset", "Case", "GoldenCase", "Scenario", "BehaviorCase",
|
|
36
|
+
"FailureCase", "AdversarialCase",
|
|
37
|
+
"UserSimulator", "ScriptedUserSimulator", "RandomUserSimulator", "LLMUserSimulator",
|
|
38
|
+
"Environment", "StatefulEnv", "GridWorld", "AdversarialSimulator",
|
|
39
|
+
"ChaosRunner", "chaos_inject", "chaos_available",
|
|
40
|
+
"Watcher", "render_metric_heatmap", "build_dashboard", "serve_dashboard",
|
|
41
|
+
"export_report",
|
|
42
|
+
"AgentRun", "Event", "Trace", "Node", "Edge",
|
|
43
|
+
"ActionEvent", "ReasoningEvent", "ToolCallEvent", "ObservationEvent",
|
|
44
|
+
"MemoryReadEvent", "MemoryWriteEvent", "ErrorEvent", "StateTransitionEvent",
|
|
45
|
+
"instrument", "trace", "event", "current_trace",
|
|
46
|
+
"metric", "Batch", "default_metrics", "layers",
|
|
47
|
+
"adapter", "find_adapter", "list_adapters",
|
|
48
|
+
"from_langchain", "from_langgraph", "from_openai", "from_crewai",
|
|
49
|
+
"from_pydanticai", "from_autogen", "from_google_adk", "from_mcp", "from_custom",
|
|
50
|
+
"exporter", "discover",
|
|
51
|
+
"assert_stable",
|
|
52
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Benchmarks package."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .bench import (
|
|
5
|
+
load_ref, load_dataset, benchmark_variants, benchmark_refs, to_markdown,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"load_ref", "load_dataset", "benchmark_variants", "benchmark_refs", "to_markdown",
|
|
10
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Benchmarking (spec §14): compare models/prompts/frameworks/agents/versions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import importlib
|
|
5
|
+
from typing import Any, Callable, Dict, List, Tuple
|
|
6
|
+
|
|
7
|
+
from ..core.suite import Suite
|
|
8
|
+
from ..datasets.loaders import Dataset
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_ref(ref: str):
|
|
12
|
+
"""Load ``module:attr`` (agent/function) or ``module:attr`` callable."""
|
|
13
|
+
mod_name, attr = ref.split(":")
|
|
14
|
+
mod = importlib.import_module(mod_name)
|
|
15
|
+
return getattr(mod, attr)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_dataset(path: str) -> Dataset:
|
|
19
|
+
p = path.lower()
|
|
20
|
+
if p.endswith(".jsonl"):
|
|
21
|
+
return Dataset.from_jsonl(path)
|
|
22
|
+
if p.endswith((".yaml", ".yml")):
|
|
23
|
+
return Dataset.from_yaml(path)
|
|
24
|
+
if p.endswith(".csv"):
|
|
25
|
+
return Dataset.from_csv(path)
|
|
26
|
+
return Dataset.from_json(path)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def benchmark_variants(
|
|
30
|
+
variants: List[Tuple[str, Callable]], dataset, trials: int = 100, seed: int = 0,
|
|
31
|
+
) -> List[Tuple[str, dict]]:
|
|
32
|
+
"""Run the suite for each (name, agent) variant and return rows."""
|
|
33
|
+
rows = []
|
|
34
|
+
for name, agent in variants:
|
|
35
|
+
res = Suite(seed=seed).run(agent, dataset, trials=trials)
|
|
36
|
+
rows.append((name, res))
|
|
37
|
+
return rows
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def benchmark_refs(
|
|
41
|
+
refs: List[str], dataset_path: str, trials: int = 100, seed: int = 0,
|
|
42
|
+
) -> List[Tuple[str, dict]]:
|
|
43
|
+
ds = load_dataset(dataset_path)
|
|
44
|
+
variants = [(r, load_ref(r)) for r in refs]
|
|
45
|
+
return benchmark_variants(variants, ds, trials=trials, seed=seed)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def to_markdown(rows: List[Tuple[str, dict]], metrics: List[str] | None = None) -> str:
|
|
49
|
+
if not rows:
|
|
50
|
+
return "(no results)"
|
|
51
|
+
if metrics is None:
|
|
52
|
+
metrics = [k for k, v in rows[0][1].items() if isinstance(v, (int, float))]
|
|
53
|
+
header = ["variant"] + metrics
|
|
54
|
+
lines = ["| " + " | ".join(header) + " |",
|
|
55
|
+
"| " + " | ".join("---" for _ in header) + " |"]
|
|
56
|
+
for name, res in rows:
|
|
57
|
+
cells = [name] + [f"{res.get(m, 0):.4f}" for m in metrics]
|
|
58
|
+
lines.append("| " + " | ".join(cells) + " |")
|
|
59
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Fault injection (spec §10): 8 fault types as agent wrappers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import random
|
|
5
|
+
from typing import Callable, Dict, List
|
|
6
|
+
|
|
7
|
+
from ..core.models import AgentRun, ErrorEvent, ToolCallEvent, MemoryWriteEvent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _wrap(agent: Callable, fault_fn, p: float, rng: random.Random) -> Callable:
|
|
11
|
+
def wrapped(inp):
|
|
12
|
+
if rng.random() < p:
|
|
13
|
+
return fault_fn(agent, inp)
|
|
14
|
+
return agent(inp)
|
|
15
|
+
wrapped.__name__ = getattr(fault_fn, "__name__", "fault")
|
|
16
|
+
return wrapped
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# --- individual faults ----------------------------------------------------
|
|
20
|
+
def api_fail(agent, inp):
|
|
21
|
+
raise RuntimeError("CHAOS: simulated API failure")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def timeout(agent, inp):
|
|
25
|
+
raise TimeoutError("CHAOS: simulated timeout")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def rate_limit(agent, inp):
|
|
29
|
+
raise RuntimeError("CHAOS: 429 rate limited")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def network_delay(agent, inp):
|
|
33
|
+
raise ConnectionError("CHAOS: simulated network delay/reset")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def tool_crash(agent, inp):
|
|
37
|
+
res = agent(inp)
|
|
38
|
+
if isinstance(res, AgentRun):
|
|
39
|
+
events = [ToolCallEvent("__crashed__") if e.type == "tool" else e
|
|
40
|
+
for e in res.events]
|
|
41
|
+
return AgentRun(run_id=res.run_id, input=inp, output=res.output,
|
|
42
|
+
events=events, cost=res.cost,
|
|
43
|
+
metadata={**res.metadata, "error": "tool_crash"})
|
|
44
|
+
return AgentRun(run_id="", input=inp, output=res,
|
|
45
|
+
events=[ToolCallEvent("__crashed__")], metadata={"error": "tool_crash"})
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def memory_corrupt(agent, inp):
|
|
49
|
+
res = agent(inp)
|
|
50
|
+
if isinstance(res, AgentRun):
|
|
51
|
+
events = [MemoryWriteEvent(e.name, {**e.data, "polluting": True})
|
|
52
|
+
if e.type == "memory_write" else e for e in res.events]
|
|
53
|
+
return AgentRun(run_id=res.run_id, input=inp, output=res.output,
|
|
54
|
+
events=events, cost=res.cost, metadata=res.metadata)
|
|
55
|
+
return res
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def token_limit(agent, inp, limit: int = 16):
|
|
59
|
+
res = agent(inp)
|
|
60
|
+
out = res.output if isinstance(res, AgentRun) else res
|
|
61
|
+
text = out if isinstance(out, str) else str(out)
|
|
62
|
+
if len(text) > limit:
|
|
63
|
+
truncated = text[:limit]
|
|
64
|
+
if isinstance(res, AgentRun):
|
|
65
|
+
return AgentRun(run_id=res.run_id, input=inp, output=truncated,
|
|
66
|
+
events=res.events, cost=res.cost,
|
|
67
|
+
metadata={**res.metadata, "error": "token_limit"})
|
|
68
|
+
return AgentRun(run_id="", input=inp, output=truncated, metadata={"error": "token_limit"})
|
|
69
|
+
return res
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def malformed_output(agent, inp):
|
|
73
|
+
res = agent(inp)
|
|
74
|
+
if isinstance(res, AgentRun):
|
|
75
|
+
return AgentRun(run_id=res.run_id, input=inp, output="[[MALFORMED]]",
|
|
76
|
+
events=res.events, cost=res.cost,
|
|
77
|
+
metadata={**res.metadata, "error": "malformed"})
|
|
78
|
+
return AgentRun(run_id="", input=inp, output="[[MALFORMED]]", metadata={"error": "malformed"})
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
FAULTS: Dict[str, Callable] = {
|
|
82
|
+
"api_fail": api_fail,
|
|
83
|
+
"timeout": timeout,
|
|
84
|
+
"rate_limit": rate_limit,
|
|
85
|
+
"network_delay": network_delay,
|
|
86
|
+
"tool_crash": tool_crash,
|
|
87
|
+
"memory_corrupt": memory_corrupt,
|
|
88
|
+
"token_limit": token_limit,
|
|
89
|
+
"malformed_output": malformed_output,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def inject(agent: Callable, fault: str, p: float = 1.0, seed: int = 0) -> Callable:
|
|
94
|
+
"""Wrap ``agent`` so fault ``fault`` fires with probability ``p``."""
|
|
95
|
+
if fault not in FAULTS:
|
|
96
|
+
raise ValueError(f"unknown fault {fault!r}; have {sorted(FAULTS)}")
|
|
97
|
+
return _wrap(agent, FAULTS[fault], p, random.Random(seed))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def available() -> List[str]:
|
|
101
|
+
return list(FAULTS)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Chaos runner (spec §10): run the suite under each fault and compare."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import random
|
|
5
|
+
from typing import Callable, Iterable, List
|
|
6
|
+
|
|
7
|
+
from ..core.suite import Suite
|
|
8
|
+
from .faults import FAULTS, inject
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ChaosRunner:
|
|
12
|
+
"""Run an agent under baseline and each requested fault."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, seed: int = 0):
|
|
15
|
+
self.seed = seed
|
|
16
|
+
|
|
17
|
+
def run(self, agent: Callable, dataset, faults: List[str] | str,
|
|
18
|
+
trials: int = 50) -> dict:
|
|
19
|
+
if isinstance(faults, str):
|
|
20
|
+
faults = [faults]
|
|
21
|
+
unknown = [f for f in faults if f not in FAULTS]
|
|
22
|
+
if unknown:
|
|
23
|
+
raise ValueError(f"unknown faults: {unknown}")
|
|
24
|
+
rng = random.Random(self.seed)
|
|
25
|
+
baseline = Suite(seed=self.seed).run(agent, dataset, trials=trials)
|
|
26
|
+
out = {"baseline": baseline, "faults": {}}
|
|
27
|
+
for f in faults:
|
|
28
|
+
wrapped = inject(agent, f, p=1.0, seed=self.seed)
|
|
29
|
+
out["faults"][f] = Suite(seed=self.seed).run(wrapped, dataset, trials=trials)
|
|
30
|
+
return out
|
|
31
|
+
|
|
32
|
+
def summary(self, result: dict) -> str:
|
|
33
|
+
lines = [f"{'fault':16} {'success':>9} {'fail':>9} {'drift':>9}"]
|
|
34
|
+
b = result["baseline"]
|
|
35
|
+
lines.append(f"{'(baseline)':16} {b['success_rate']:9.4f} "
|
|
36
|
+
f"{1 - b['success_rate']:9.4f} {b['drift_score']:9.4f}")
|
|
37
|
+
for f, r in result["faults"].items():
|
|
38
|
+
lines.append(f"{f:16} {r['success_rate']:9.4f} "
|
|
39
|
+
f"{1 - r['success_rate']:9.4f} {r['drift_score']:9.4f}")
|
|
40
|
+
return "\n".join(lines)
|