Skip to content

Commit 0ec01c2

Browse files
authored
Add files via upload
1 parent 5185590 commit 0ec01c2

12 files changed

Lines changed: 869 additions & 4 deletions

LICENSE

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
MIT License
22

3-
Copyright (c) 2025 Manish Shukla
3+
Copyright (c) 2025 Manish A. Shukla
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
6-
of this software and associated documentation files (the "Software"), to deal
6+
of this software and associated documentation files (the Software), to deal
77
in the Software without restriction, including without limitation the rights
88
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99
copies of the Software, and to permit persons to whom the Software is
@@ -12,10 +12,10 @@ furnished to do so, subject to the following conditions:
1212
The above copyright notice and this permission notice shall be included in all
1313
copies or substantial portions of the Software.
1414

15-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1717
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21-
SOFTWARE.
21+
SOFTWARE.

README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Adaptive Multi‑Dimensional Monitoring (AMDM)
2+
3+
This repository contains a reference implementation of the **Adaptive Multi‑Dimensional Monitoring (AMDM)** algorithm described in our paper *Adaptive Monitoring and Real‑World Evaluation of Agentic AI Systems* (Advanced version). AMDM is designed to detect anomalies across multiple axes (e.g., capability, robustness, safety, human factors, economics) in streaming logs generated by agentic AI systems.
4+
5+
## Features
6+
7+
* **Rolling normalisation**: Maintains rolling means and standard deviations for each metric to compute per‑metric z‑scores.
8+
* **EWMA thresholds**: Computes exponentially weighted moving averages (EWMAs) per axis and flags per‑axis anomalies when deviations exceed a configurable threshold.
9+
* **Joint anomaly detection**: Maintains a joint mean and covariance matrix of the axis scores and computes a Mahalanobis distance; flags joint anomalies when the distance exceeds a chi‑square threshold.
10+
* **Calibration tools**: Includes a simple method to calibrate parameters (window length, EWMA smoothing and joint threshold) based on a quiet period of normal operation.
11+
* **Synthetic data generator**: Provides a `simulate.py` script to produce synthetic event streams with injected anomalies for demonstration purposes.
12+
13+
## Repository structure
14+
15+
```
16+
amdm_repo/
17+
├── amdm.py # Core implementation of the AMDM algorithm
18+
├── simulate.py # Generates synthetic data and runs AMDM for a demo
19+
├── example_data.csv # Sample metrics used in the demo
20+
├── README.md # Project description and usage instructions
21+
├── LICENSE # MIT license
22+
└── .gitignore # Files to ignore in git
23+
```
24+
25+
## Installation
26+
27+
This project requires Python 3.8 or later and the following packages:
28+
29+
```
30+
numpy
31+
scipy
32+
matplotlib (optional, for plotting in the demo)
33+
```
34+
35+
You can install the dependencies using `pip`:
36+
37+
```bash
38+
pip install numpy scipy matplotlib
39+
```
40+
41+
## Usage
42+
43+
### Running the demo
44+
45+
The `simulate.py` script generates a synthetic event stream with four metrics spanning two axes (capability and safety) and injects goal‑drift and safety‑violation anomalies. It then runs AMDM on the stream and prints detected anomalies.
46+
47+
```bash
48+
python simulate.py
49+
```
50+
51+
You should see output indicating when per‑axis and joint anomalies are detected. The script also produces a simple plot (requires `matplotlib`) showing the axis scores over time.
52+
53+
### Integrating into your system
54+
55+
1. Place your streaming logs into a pandas DataFrame where each row corresponds to a time step and each column corresponds to a metric.
56+
2. Define a dictionary mapping each metric to one of the five axes (capability, robustness, safety, human, economic).
57+
3. Instantiate the `AMDM` class with the list of metric names, the axis mapping, and desired parameters (`window_size`, `lambda_`, `alpha`, `k`).
58+
4. Call `update(metrics_dict)` on each time step. The method returns flags indicating per‑axis anomalies and joint anomalies.
59+
60+
## License
61+
62+
This repository is licensed under the MIT License. See `LICENSE` for details.
63+
64+
## Citation
65+
66+
If you use this code in your research, please cite our paper:
67+
68+
```
69+
M. A. Shukla. “Adaptive Monitoring and Real‑World Evaluation of Agentic AI Systems,” 2025.
70+
```

amdm.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
"""
2+
Implementation of the Adaptive Multi‑Dimensional Monitoring (AMDM) algorithm.
3+
4+
This module defines classes to track streaming metrics across multiple axes and
5+
detect anomalies using per‑axis EWMA thresholding and joint Mahalanobis
6+
distance. It is intended for demonstration purposes and is not optimised for
7+
production use.
8+
9+
Example:
10+
11+
>>> from amdm import AMDM
12+
>>> metric_names = ["latency", "throughput", "error_rate", "toxicity"]
13+
>>> axis_map = {"latency": "capability", "throughput": "capability",
14+
... "error_rate": "robustness", "toxicity": "safety"}
15+
>>> monitor = AMDM(metric_names, axis_map)
16+
>>> for metrics in stream: # metrics is a dict with values for each metric
17+
... axis_flags, joint_flag = monitor.update(metrics)
18+
... if joint_flag:
19+
... print("Joint anomaly detected at step", monitor.t)
20+
21+
"""
22+
from __future__ import annotations
23+
24+
from collections import deque, defaultdict
25+
from typing import Dict, List, Tuple
26+
27+
import numpy as np
28+
from scipy.stats import chi2
29+
30+
31+
class RollingStats:
32+
"""Maintain rolling mean and standard deviation for a sequence of numbers."""
33+
34+
def __init__(self, window_size: int = 50) -> None:
35+
self.window_size = window_size
36+
self.window = deque(maxlen=window_size)
37+
self._sum = 0.0
38+
self._sum_sq = 0.0
39+
40+
def update(self, value: float) -> Tuple[float, float]:
41+
"""Add a new value and return the updated mean and std."""
42+
# Remove oldest value if window is full
43+
if len(self.window) == self.window.maxlen:
44+
old = self.window.popleft()
45+
self._sum -= old
46+
self._sum_sq -= old * old
47+
# Add new value
48+
self.window.append(value)
49+
self._sum += value
50+
self._sum_sq += value * value
51+
# Compute mean and std
52+
n = len(self.window)
53+
mean = self._sum / n
54+
# Variance with Bessel's correction
55+
if n > 1:
56+
var = (self._sum_sq - n * mean * mean) / (n - 1)
57+
var = max(var, 1e-12)
58+
std = var ** 0.5
59+
else:
60+
std = 1e-6 # avoid zero std
61+
return mean, std
62+
63+
64+
class AMDM:
65+
"""Adaptive Multi‑Dimensional Monitoring for streaming metrics."""
66+
67+
def __init__(
68+
self,
69+
metric_names: List[str],
70+
axis_map: Dict[str, str],
71+
window_size: int = 50,
72+
lambda_: float = 0.25,
73+
k: float = 2.0,
74+
alpha: float = 0.01,
75+
) -> None:
76+
"""
77+
Parameters
78+
----------
79+
metric_names: list of metric names (strings). Must match keys in the
80+
axis_map.
81+
axis_map: mapping from metric name to axis name.
82+
window_size: number of recent values to use for rolling statistics.
83+
lambda_: EWMA smoothing factor (0 < lambda_ <= 1).
84+
k: per‑axis anomaly multiplier (in units of standard deviations).
85+
alpha: desired joint false‑alarm rate (for chi‑square threshold).
86+
"""
87+
self.metric_names = list(metric_names)
88+
self.axis_map = dict(axis_map)
89+
# Validate mapping
90+
for m in self.metric_names:
91+
if m not in self.axis_map:
92+
raise ValueError(f"Metric {m} has no assigned axis.")
93+
self.axes: List[str] = sorted(set(axis_map.values()))
94+
self.window_size = window_size
95+
self.lambda_ = lambda_
96+
self.k = k
97+
self.alpha = alpha
98+
# Rolling stats per metric
99+
self.stats = {m: RollingStats(window_size) for m in self.metric_names}
100+
# EWMA per axis (initially None)
101+
self.axis_ewma: Dict[str, float] = {a: None for a in self.axes}
102+
# Rolling std of axis scores
103+
self.axis_std: Dict[str, float] = {a: 1e-6 for a in self.axes}
104+
# Mahalanobis stats
105+
self.n_joint = 0
106+
self.joint_mean = np.zeros(len(self.axes))
107+
self.joint_cov = np.eye(len(self.axes))
108+
# Precompute chi‑square threshold
109+
self.chi2_thresh = chi2.ppf(1.0 - alpha, df=len(self.axes))
110+
# Time step
111+
self.t = 0
112+
113+
def update(self, metrics: Dict[str, float]) -> Tuple[Dict[str, bool], bool]:
114+
"""
115+
Update the monitor with a new set of metrics.
116+
117+
Parameters
118+
----------
119+
metrics: dict mapping metric names to values at the current time step.
120+
121+
Returns
122+
-------
123+
axis_flags: dict mapping axis names to booleans indicating whether
124+
a per‑axis anomaly was detected at this step.
125+
joint_flag: bool indicating whether a joint anomaly was detected.
126+
"""
127+
self.t += 1
128+
# Compute per‑metric z‑scores
129+
z_scores = {}
130+
for m in self.metric_names:
131+
mean, std = self.stats[m].update(metrics[m])
132+
z_scores[m] = (metrics[m] - mean) / max(std, 1e-6)
133+
# Aggregate z‑scores into axis scores (mean of metrics in axis)
134+
axis_scores: Dict[str, float] = defaultdict(list)
135+
for m, z in z_scores.items():
136+
axis = self.axis_map[m]
137+
axis_scores[axis].append(z)
138+
for a in axis_scores:
139+
axis_scores[a] = float(np.mean(axis_scores[a]))
140+
# Update EWMA and std per axis; flag anomalies
141+
axis_flags: Dict[str, bool] = {}
142+
for a in self.axes:
143+
score = axis_scores.get(a, 0.0)
144+
if self.axis_ewma[a] is None:
145+
# initialise EWMA and std
146+
self.axis_ewma[a] = score
147+
self.axis_std[a] = 1e-6
148+
axis_flags[a] = False
149+
else:
150+
# Update EWMA
151+
prev_ewma = self.axis_ewma[a]
152+
ewma = self.lambda_ * score + (1.0 - self.lambda_) * prev_ewma
153+
# Update rolling std of axis score using simple exponential smoothing
154+
# We approximate std via EWMA of squared deviations
155+
prev_var = self.axis_std[a] ** 2
156+
var = self.lambda_ * (score - ewma) ** 2 + (1.0 - self.lambda_) * prev_var
157+
std_axis = max(var ** 0.5, 1e-6)
158+
# Determine if per‑axis anomaly
159+
axis_flags[a] = abs(score - ewma) > self.k * std_axis
160+
# Store updates
161+
self.axis_ewma[a] = ewma
162+
self.axis_std[a] = std_axis
163+
# Form joint vector of axis scores in fixed axis order
164+
joint_vector = np.array([axis_scores.get(a, 0.0) for a in self.axes])
165+
# Update joint mean and covariance using incremental formula
166+
self.n_joint += 1
167+
if self.n_joint == 1:
168+
self.joint_mean = joint_vector.copy()
169+
self.joint_cov = np.eye(len(self.axes)) * 1e-6
170+
joint_flag = False
171+
else:
172+
# Update mean
173+
delta = joint_vector - self.joint_mean
174+
new_mean = self.joint_mean + delta / self.n_joint
175+
# Update covariance using Welford's algorithm
176+
self.joint_cov += np.outer(joint_vector - new_mean, joint_vector - self.joint_mean)
177+
self.joint_mean = new_mean
178+
# Compute covariance matrix
179+
cov_mat = self.joint_cov / max(self.n_joint - 1, 1)
180+
# Compute Mahalanobis distance
181+
try:
182+
cov_inv = np.linalg.inv(cov_mat + np.eye(len(self.axes)) * 1e-6)
183+
except np.linalg.LinAlgError:
184+
cov_inv = np.linalg.pinv(cov_mat + np.eye(len(self.axes)) * 1e-6)
185+
diff = joint_vector - self.joint_mean
186+
d2 = float(diff.T @ cov_inv @ diff)
187+
joint_flag = d2 > self.chi2_thresh
188+
return axis_flags, joint_flag

eval_deployment.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env python
2+
"""
3+
eval_deployment.py
4+
===================
5+
6+
This script evaluates the AMDM algorithm on a CSV log file. Each column in
7+
the CSV corresponds to a metric and each row corresponds to a time step. A
8+
JSON file can optionally provide a mapping from metric names to axes; if none
9+
is provided, a default mapping is used (capability for the first two metrics,
10+
robustness for the third and safety for the fourth).
11+
12+
Usage:
13+
14+
```bash
15+
python eval_deployment.py --csv path/to/log.csv --mapping path/to/axis_map.json
16+
```
17+
"""
18+
from __future__ import annotations
19+
20+
import argparse
21+
import csv
22+
import json
23+
from typing import Dict, List
24+
25+
from amdm import AMDM
26+
27+
28+
def load_csv(path: str) -> List[Dict[str, float]]:
29+
stream: List[Dict[str, float]] = []
30+
with open(path, newline="") as f:
31+
reader = csv.DictReader(f)
32+
for row in reader:
33+
metrics = {k: float(v) for k, v in row.items() if v != ""}
34+
stream.append(metrics)
35+
return stream
36+
37+
38+
def main(csv_path: str, mapping_path: str | None) -> None:
39+
stream = load_csv(csv_path)
40+
metric_names = list(stream[0].keys())
41+
if mapping_path:
42+
with open(mapping_path) as f:
43+
axis_map = json.load(f)
44+
else:
45+
# Simple default: first half metrics -> capability, second -> robustness or safety
46+
axes = ["capability", "capability", "robustness", "safety"]
47+
axis_map = {m: axes[i % len(axes)] for i, m in enumerate(metric_names)}
48+
monitor = AMDM(metric_names, axis_map)
49+
print(f"Evaluating {len(stream)} steps with metrics: {metric_names}")
50+
for t, metrics in enumerate(stream, start=1):
51+
axis_flags, joint_flag = monitor.update(metrics)
52+
if joint_flag or any(axis_flags.values()):
53+
flagged_axes = [a for a, f in axis_flags.items() if f]
54+
print(f"t={t:4d}: Anomaly detected; axes={flagged_axes}, joint={joint_flag}")
55+
56+
57+
if __name__ == "__main__":
58+
parser = argparse.ArgumentParser(description="Evaluate AMDM on a CSV log file.")
59+
parser.add_argument("--csv", required=True, help="Path to CSV file containing metrics.")
60+
parser.add_argument("--mapping", help="Path to JSON file mapping metric names to axis names.")
61+
args = parser.parse_args()
62+
main(args.csv, args.mapping)

0 commit comments

Comments
 (0)