madcop 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.
- madcop-0.1.0/LICENSE +21 -0
- madcop-0.1.0/PKG-INFO +201 -0
- madcop-0.1.0/README.md +168 -0
- madcop-0.1.0/madcop/__init__.py +3 -0
- madcop-0.1.0/madcop/__main__.py +123 -0
- madcop-0.1.0/madcop/adapters/__init__.py +0 -0
- madcop-0.1.0/madcop/adapters/base.py +82 -0
- madcop-0.1.0/madcop/adapters/wms.py +94 -0
- madcop-0.1.0/madcop/anomaly/__init__.py +0 -0
- madcop-0.1.0/madcop/anomaly/detector.py +88 -0
- madcop-0.1.0/madcop/anomaly/rules.py +387 -0
- madcop-0.1.0/madcop/event.py +160 -0
- madcop-0.1.0/madcop/graph/__init__.py +0 -0
- madcop-0.1.0/madcop/rca/graph.py +263 -0
- madcop-0.1.0/madcop/rca/seed.py +71 -0
- madcop-0.1.0/madcop/strategy/__init__.py +0 -0
- madcop-0.1.0/madcop.egg-info/PKG-INFO +201 -0
- madcop-0.1.0/madcop.egg-info/SOURCES.txt +24 -0
- madcop-0.1.0/madcop.egg-info/dependency_links.txt +1 -0
- madcop-0.1.0/madcop.egg-info/requires.txt +6 -0
- madcop-0.1.0/madcop.egg-info/top_level.txt +1 -0
- madcop-0.1.0/pyproject.toml +56 -0
- madcop-0.1.0/setup.cfg +4 -0
- madcop-0.1.0/tests/test_anomaly.py +284 -0
- madcop-0.1.0/tests/test_event.py +163 -0
- madcop-0.1.0/tests/test_rca.py +170 -0
madcop-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lin Ruihan
|
|
4
|
+
|
|
5
|
+
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
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
madcop-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: madcop
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: madcop — the supply chain cop that goes mad for anomalies. Pluggable LangGraph framework: detect, diagnose, decide.
|
|
5
|
+
Author-email: Lin Ruihan <chuiniu@me.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/linmy666/madcop
|
|
8
|
+
Project-URL: Repository, https://github.com/linmy666/madcop
|
|
9
|
+
Project-URL: Issues, https://github.com/linmy666/madcop/issues
|
|
10
|
+
Keywords: supply-chain,langgraph,agent,anomaly-detection,rca,root-cause-analysis,ai-pm
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Office/Business
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: rich>=13.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
30
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
31
|
+
Requires-Dist: twine>=4.0; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# madcop
|
|
35
|
+
|
|
36
|
+
> **mad** + **cop** — the supply chain cop that goes *mad* for anomalies.
|
|
37
|
+
> Pluggable LangGraph framework: from "detect" to "diagnose" to "decide", with self-evolution.
|
|
38
|
+
|
|
39
|
+
[](#tests)
|
|
40
|
+
[](#requirements)
|
|
41
|
+
[](#license)
|
|
42
|
+
[](https://test.pypi.org/project/madcop/)
|
|
43
|
+
|
|
44
|
+
## What is madcop?
|
|
45
|
+
|
|
46
|
+
**madcop** is a pluggable framework that turns raw supply chain telemetry
|
|
47
|
+
(orders, shipments, warehouse readings, contracts) into **decision prompts**
|
|
48
|
+
with full causal chains. Where most tools stop at "alert fired", madcop walks
|
|
49
|
+
the chain back to the **human decision** that made the anomaly possible.
|
|
50
|
+
|
|
51
|
+
The name is short for **mad cop** — a cop that goes mad for anomalies. Not
|
|
52
|
+
in a punitive sense, but in the sense of "won't let a single anomaly go
|
|
53
|
+
untraced to its source."
|
|
54
|
+
|
|
55
|
+
## Why?
|
|
56
|
+
|
|
57
|
+
A typical supply chain alert reads:
|
|
58
|
+
|
|
59
|
+
> ⚠️ Cold-chain temperature exceeded threshold at 14:30.
|
|
60
|
+
|
|
61
|
+
That tells you **what** happened, not **why** it could happen. madcop answers
|
|
62
|
+
the second question:
|
|
63
|
+
|
|
64
|
+
> The temperature breach on SHIP-2026-0615-CG-SH traces back to a BD
|
|
65
|
+
> decision (DEC-2026-03-12-N3) made three months earlier to accept the
|
|
66
|
+
> supplier's "fine equals exemption" concession. That decision shaped
|
|
67
|
+
> CLAUSE-04 — a **passive** clause that punishes the breach but does not
|
|
68
|
+
> prevent it. The contract was signed with 冷链速运 at Q1 cost-cutting
|
|
69
|
+
> pressure, and the shipment is now exposed to the same failure mode.
|
|
70
|
+
|
|
71
|
+
The PM framing: an alert without a cause is a notification. An alert with a
|
|
72
|
+
cause is a **decision prompt**. madcop's job is to bridge the two.
|
|
73
|
+
|
|
74
|
+
## Architecture (4 layers, 1 graph)
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
┌──────────────────────────────────────────────────────┐
|
|
78
|
+
│ L4 Strategy Router — zero-code YAML policies │
|
|
79
|
+
├──────────────────────────────────────────────────────┤
|
|
80
|
+
│ L3 LangGraph — detect → diagnose → decide │
|
|
81
|
+
│ → learn (state machine) │
|
|
82
|
+
├──────────────────────────────────────────────────────┤
|
|
83
|
+
│ L2 Anomaly Engine — rules · RCA · counterfactual│
|
|
84
|
+
├──────────────────────────────────────────────────────┤
|
|
85
|
+
│ L1 Unified Data Layer — OMS/TMS/WMS/BMS adapters │
|
|
86
|
+
└──────────────────────────────────────────────────────┘
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**L1 — Unified Data Layer** (`madcop/event.py`, `madcop/adapters/`)
|
|
90
|
+
A `UnifiedEvent` is the lingua franca. Every adapter implements `BaseAdapter`
|
|
91
|
+
and yields events with frozen, UTC-validated, severity-rated fields.
|
|
92
|
+
|
|
93
|
+
**L2 — Anomaly Engine** (`madcop/anomaly/`, `madcop/rca/`)
|
|
94
|
+
5 shipped rules + a `Detector` that orchestrates them. RCA walks a typed
|
|
95
|
+
property graph from any finding back to a decision.
|
|
96
|
+
|
|
97
|
+
**L3 — LangGraph Orchestrator** (`madcop/graph/`) — *planned, W5*
|
|
98
|
+
A typed state machine that sequences detect → diagnose → decide → learn.
|
|
99
|
+
|
|
100
|
+
**L4 — Strategy Router** (`madcop/strategy/`) — *planned, W7*
|
|
101
|
+
YAML policies + feedback-weighted registry. Self-evolution is real here:
|
|
102
|
+
weekly reports roll up the week's findings, and policies are ranked by
|
|
103
|
+
their rolling effectiveness.
|
|
104
|
+
|
|
105
|
+
## What's shipped today (W1 + W2 + W3)
|
|
106
|
+
|
|
107
|
+
| Layer | Component | Status |
|
|
108
|
+
|-------|-----------|--------|
|
|
109
|
+
| L1 | `UnifiedEvent` with UTC + severity + source/event_type validation | ✅ |
|
|
110
|
+
| L1 | `BaseAdapter` contract + WMS mock (cold-chain) | ✅ |
|
|
111
|
+
| L2 | `Detector` + 5 rules (cold-chain temp / sustained / OMS cancel / TMS lead / BMS score) | ✅ |
|
|
112
|
+
| L2 | `KnowledgeGraph` + `trace()` + `explain()` RCA | ✅ |
|
|
113
|
+
| L2 | Cold-chain seed graph (5 nodes, 4 edges) | ✅ |
|
|
114
|
+
| L4 | Strategy registry, weekly report, LLM backend | 🔜 W7 |
|
|
115
|
+
|
|
116
|
+
## Installation
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pip install madcop
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Or from TestPyPI (the current published version):
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pip install --index-url https://test.pypi.org/simple/ madcop
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Quick start
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# W1: see the raw event stream
|
|
132
|
+
python -m madcop run coldchain
|
|
133
|
+
|
|
134
|
+
# W2: detect anomalies
|
|
135
|
+
python -m madcop run anomalies
|
|
136
|
+
|
|
137
|
+
# W3: detect + trace each finding to a root cause (the headline feature)
|
|
138
|
+
python -m madcop run rca
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### What `run rca` looks like
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
madcop RCA demo — 3 finding(s) on SHIP-2026-0615-CG-SH
|
|
145
|
+
|
|
146
|
+
━━━ finding 1/3 ━━━
|
|
147
|
+
rule: wms.coldchain.temperature_breach
|
|
148
|
+
summary: Cold-chain temperature -14.2°C exceeds threshold -15.0°C by 0.8°C
|
|
149
|
+
chain: 5 step(s), root cause:
|
|
150
|
+
╭──────────────────────────────── root cause ────────────────────────────────╮
|
|
151
|
+
│ decision DEC-2026-03-12-N3 (BD 接受乙方'罚款即免责'让步) (by BD-Lin) — │
|
|
152
|
+
│ rationale: Q1 降本压力 → shaped clause CLAUSE-04 (温控异常通知条款) PASSIVE │
|
|
153
|
+
│ ("温控异常时, 承运商应在 30 分钟内书面通知甲方, 逾期每日扣 0.5% 服务费") → │
|
|
154
|
+
│ under contract CONT-2026-0312 (冷链速运 / 2026 年度框架) → carried by │
|
|
155
|
+
│ 冷链速运 → on shipment SHIP-2026-0615-CG-SH (广州→上海, 冷链, 2026-06-15) │
|
|
156
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Tests
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
pip install -e ".[dev]"
|
|
163
|
+
pytest
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**45 tests, all passing.** They cover the L1 contract (UTC validation, event
|
|
167
|
+
type / source system consistency, adapter behavior), the L2 detector (every
|
|
168
|
+
rule, plus state-machine semantics for windowed rules), and the RCA graph
|
|
169
|
+
(forward/reverse traversal, empty chain, unknown subject).
|
|
170
|
+
|
|
171
|
+
## Roadmap
|
|
172
|
+
|
|
173
|
+
See [`ROADMAP.md`](ROADMAP.md). 8 weeks, 1 commit per week. Current: **W3
|
|
174
|
+
done, ready to push**.
|
|
175
|
+
|
|
176
|
+
## Requirements
|
|
177
|
+
|
|
178
|
+
- Python 3.10+
|
|
179
|
+
- `rich >= 13.0`
|
|
180
|
+
|
|
181
|
+
## Project status
|
|
182
|
+
|
|
183
|
+
Alpha. The architecture is real, the data layer is real, the anomaly rules
|
|
184
|
+
are real, and the RCA traces are real. The adapters are mock data today;
|
|
185
|
+
real wire integrations to OMS / TMS / WMS / BMS systems are scoped for
|
|
186
|
+
later (see Roadmap).
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT. See [`LICENSE`](LICENSE).
|
|
191
|
+
|
|
192
|
+
## Why "madcop"?
|
|
193
|
+
|
|
194
|
+
When the user asked for a name for "the agent that goes mad for anomalies",
|
|
195
|
+
the obvious answer was **mad + cop**. The product is a cop that goes mad for
|
|
196
|
+
anomalies — not in a punitive sense, but in the sense of "won't let a
|
|
197
|
+
single anomaly go untraced to its source."
|
|
198
|
+
|
|
199
|
+
## Contact
|
|
200
|
+
|
|
201
|
+
Lin Ruihan · chuiniu@me.com
|
madcop-0.1.0/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# madcop
|
|
2
|
+
|
|
3
|
+
> **mad** + **cop** — the supply chain cop that goes *mad* for anomalies.
|
|
4
|
+
> Pluggable LangGraph framework: from "detect" to "diagnose" to "decide", with self-evolution.
|
|
5
|
+
|
|
6
|
+
[](#tests)
|
|
7
|
+
[](#requirements)
|
|
8
|
+
[](#license)
|
|
9
|
+
[](https://test.pypi.org/project/madcop/)
|
|
10
|
+
|
|
11
|
+
## What is madcop?
|
|
12
|
+
|
|
13
|
+
**madcop** is a pluggable framework that turns raw supply chain telemetry
|
|
14
|
+
(orders, shipments, warehouse readings, contracts) into **decision prompts**
|
|
15
|
+
with full causal chains. Where most tools stop at "alert fired", madcop walks
|
|
16
|
+
the chain back to the **human decision** that made the anomaly possible.
|
|
17
|
+
|
|
18
|
+
The name is short for **mad cop** — a cop that goes mad for anomalies. Not
|
|
19
|
+
in a punitive sense, but in the sense of "won't let a single anomaly go
|
|
20
|
+
untraced to its source."
|
|
21
|
+
|
|
22
|
+
## Why?
|
|
23
|
+
|
|
24
|
+
A typical supply chain alert reads:
|
|
25
|
+
|
|
26
|
+
> ⚠️ Cold-chain temperature exceeded threshold at 14:30.
|
|
27
|
+
|
|
28
|
+
That tells you **what** happened, not **why** it could happen. madcop answers
|
|
29
|
+
the second question:
|
|
30
|
+
|
|
31
|
+
> The temperature breach on SHIP-2026-0615-CG-SH traces back to a BD
|
|
32
|
+
> decision (DEC-2026-03-12-N3) made three months earlier to accept the
|
|
33
|
+
> supplier's "fine equals exemption" concession. That decision shaped
|
|
34
|
+
> CLAUSE-04 — a **passive** clause that punishes the breach but does not
|
|
35
|
+
> prevent it. The contract was signed with 冷链速运 at Q1 cost-cutting
|
|
36
|
+
> pressure, and the shipment is now exposed to the same failure mode.
|
|
37
|
+
|
|
38
|
+
The PM framing: an alert without a cause is a notification. An alert with a
|
|
39
|
+
cause is a **decision prompt**. madcop's job is to bridge the two.
|
|
40
|
+
|
|
41
|
+
## Architecture (4 layers, 1 graph)
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
┌──────────────────────────────────────────────────────┐
|
|
45
|
+
│ L4 Strategy Router — zero-code YAML policies │
|
|
46
|
+
├──────────────────────────────────────────────────────┤
|
|
47
|
+
│ L3 LangGraph — detect → diagnose → decide │
|
|
48
|
+
│ → learn (state machine) │
|
|
49
|
+
├──────────────────────────────────────────────────────┤
|
|
50
|
+
│ L2 Anomaly Engine — rules · RCA · counterfactual│
|
|
51
|
+
├──────────────────────────────────────────────────────┤
|
|
52
|
+
│ L1 Unified Data Layer — OMS/TMS/WMS/BMS adapters │
|
|
53
|
+
└──────────────────────────────────────────────────────┘
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**L1 — Unified Data Layer** (`madcop/event.py`, `madcop/adapters/`)
|
|
57
|
+
A `UnifiedEvent` is the lingua franca. Every adapter implements `BaseAdapter`
|
|
58
|
+
and yields events with frozen, UTC-validated, severity-rated fields.
|
|
59
|
+
|
|
60
|
+
**L2 — Anomaly Engine** (`madcop/anomaly/`, `madcop/rca/`)
|
|
61
|
+
5 shipped rules + a `Detector` that orchestrates them. RCA walks a typed
|
|
62
|
+
property graph from any finding back to a decision.
|
|
63
|
+
|
|
64
|
+
**L3 — LangGraph Orchestrator** (`madcop/graph/`) — *planned, W5*
|
|
65
|
+
A typed state machine that sequences detect → diagnose → decide → learn.
|
|
66
|
+
|
|
67
|
+
**L4 — Strategy Router** (`madcop/strategy/`) — *planned, W7*
|
|
68
|
+
YAML policies + feedback-weighted registry. Self-evolution is real here:
|
|
69
|
+
weekly reports roll up the week's findings, and policies are ranked by
|
|
70
|
+
their rolling effectiveness.
|
|
71
|
+
|
|
72
|
+
## What's shipped today (W1 + W2 + W3)
|
|
73
|
+
|
|
74
|
+
| Layer | Component | Status |
|
|
75
|
+
|-------|-----------|--------|
|
|
76
|
+
| L1 | `UnifiedEvent` with UTC + severity + source/event_type validation | ✅ |
|
|
77
|
+
| L1 | `BaseAdapter` contract + WMS mock (cold-chain) | ✅ |
|
|
78
|
+
| L2 | `Detector` + 5 rules (cold-chain temp / sustained / OMS cancel / TMS lead / BMS score) | ✅ |
|
|
79
|
+
| L2 | `KnowledgeGraph` + `trace()` + `explain()` RCA | ✅ |
|
|
80
|
+
| L2 | Cold-chain seed graph (5 nodes, 4 edges) | ✅ |
|
|
81
|
+
| L4 | Strategy registry, weekly report, LLM backend | 🔜 W7 |
|
|
82
|
+
|
|
83
|
+
## Installation
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install madcop
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Or from TestPyPI (the current published version):
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install --index-url https://test.pypi.org/simple/ madcop
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Quick start
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# W1: see the raw event stream
|
|
99
|
+
python -m madcop run coldchain
|
|
100
|
+
|
|
101
|
+
# W2: detect anomalies
|
|
102
|
+
python -m madcop run anomalies
|
|
103
|
+
|
|
104
|
+
# W3: detect + trace each finding to a root cause (the headline feature)
|
|
105
|
+
python -m madcop run rca
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### What `run rca` looks like
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
madcop RCA demo — 3 finding(s) on SHIP-2026-0615-CG-SH
|
|
112
|
+
|
|
113
|
+
━━━ finding 1/3 ━━━
|
|
114
|
+
rule: wms.coldchain.temperature_breach
|
|
115
|
+
summary: Cold-chain temperature -14.2°C exceeds threshold -15.0°C by 0.8°C
|
|
116
|
+
chain: 5 step(s), root cause:
|
|
117
|
+
╭──────────────────────────────── root cause ────────────────────────────────╮
|
|
118
|
+
│ decision DEC-2026-03-12-N3 (BD 接受乙方'罚款即免责'让步) (by BD-Lin) — │
|
|
119
|
+
│ rationale: Q1 降本压力 → shaped clause CLAUSE-04 (温控异常通知条款) PASSIVE │
|
|
120
|
+
│ ("温控异常时, 承运商应在 30 分钟内书面通知甲方, 逾期每日扣 0.5% 服务费") → │
|
|
121
|
+
│ under contract CONT-2026-0312 (冷链速运 / 2026 年度框架) → carried by │
|
|
122
|
+
│ 冷链速运 → on shipment SHIP-2026-0615-CG-SH (广州→上海, 冷链, 2026-06-15) │
|
|
123
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Tests
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pip install -e ".[dev]"
|
|
130
|
+
pytest
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**45 tests, all passing.** They cover the L1 contract (UTC validation, event
|
|
134
|
+
type / source system consistency, adapter behavior), the L2 detector (every
|
|
135
|
+
rule, plus state-machine semantics for windowed rules), and the RCA graph
|
|
136
|
+
(forward/reverse traversal, empty chain, unknown subject).
|
|
137
|
+
|
|
138
|
+
## Roadmap
|
|
139
|
+
|
|
140
|
+
See [`ROADMAP.md`](ROADMAP.md). 8 weeks, 1 commit per week. Current: **W3
|
|
141
|
+
done, ready to push**.
|
|
142
|
+
|
|
143
|
+
## Requirements
|
|
144
|
+
|
|
145
|
+
- Python 3.10+
|
|
146
|
+
- `rich >= 13.0`
|
|
147
|
+
|
|
148
|
+
## Project status
|
|
149
|
+
|
|
150
|
+
Alpha. The architecture is real, the data layer is real, the anomaly rules
|
|
151
|
+
are real, and the RCA traces are real. The adapters are mock data today;
|
|
152
|
+
real wire integrations to OMS / TMS / WMS / BMS systems are scoped for
|
|
153
|
+
later (see Roadmap).
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT. See [`LICENSE`](LICENSE).
|
|
158
|
+
|
|
159
|
+
## Why "madcop"?
|
|
160
|
+
|
|
161
|
+
When the user asked for a name for "the agent that goes mad for anomalies",
|
|
162
|
+
the obvious answer was **mad + cop**. The product is a cop that goes mad for
|
|
163
|
+
anomalies — not in a punitive sense, but in the sense of "won't let a
|
|
164
|
+
single anomaly go untraced to its source."
|
|
165
|
+
|
|
166
|
+
## Contact
|
|
167
|
+
|
|
168
|
+
Lin Ruihan · chuiniu@me.com
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""madcop CLI entry point.
|
|
2
|
+
|
|
3
|
+
Two demo scenarios today:
|
|
4
|
+
python -m madcop run coldchain # W1 — print the event stream
|
|
5
|
+
python -m madcop run anomalies coldchain # W2 — run anomaly detection
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from .adapters.wms import WMSAdapter
|
|
14
|
+
from .anomaly.rules import default_detector
|
|
15
|
+
from .rca.graph import explain, trace
|
|
16
|
+
from .rca.seed import build_coldchain_seed
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _format_event(idx: int, total: int, ev) -> str:
|
|
20
|
+
ts = ev.parsed_timestamp.strftime("%H:%M:%S")
|
|
21
|
+
val = f"{ev.value:>6.1f}°C" if ev.value is not None else " — "
|
|
22
|
+
sev = "·" * ev.severity
|
|
23
|
+
return f" [{idx:>2}/{total}] {ts} {val} sev{ev.severity} {sev} {ev.attributes.get('note', '')}"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def run_coldchain() -> int:
|
|
27
|
+
"""W1 demo: print the WMS cold-chain event stream."""
|
|
28
|
+
adapter = WMSAdapter()
|
|
29
|
+
events = sorted(adapter.fetch(), key=lambda e: e.parsed_timestamp)
|
|
30
|
+
if not events:
|
|
31
|
+
print("(no events)", file=sys.stderr)
|
|
32
|
+
return 1
|
|
33
|
+
print(f"cold-chain timeline for {events[0].subject_id}")
|
|
34
|
+
print(f" threshold: {adapter.COLD_CHAIN_THRESHOLD_C}°C")
|
|
35
|
+
print()
|
|
36
|
+
for i, ev in enumerate(events, 1):
|
|
37
|
+
print(_format_event(i, len(events), ev))
|
|
38
|
+
breaches = [e for e in events if e.value is not None and e.value > adapter.COLD_CHAIN_THRESHOLD_C]
|
|
39
|
+
print()
|
|
40
|
+
print(f" → {len(breaches)} threshold breach(es) detected")
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def run_anomalies_coldchain() -> int:
|
|
45
|
+
"""W2 demo: run all 5 anomaly rules on the cold-chain stream."""
|
|
46
|
+
adapter = WMSAdapter()
|
|
47
|
+
events = sorted(adapter.fetch(), key=lambda e: e.parsed_timestamp)
|
|
48
|
+
detector = default_detector()
|
|
49
|
+
findings = list(detector.run(events))
|
|
50
|
+
print(f"madcop anomaly report — subject={events[0].subject_id if events else 'N/A'}")
|
|
51
|
+
print(f" events: {len(events)} rules: {len(detector.rules)} findings: {len(findings)}")
|
|
52
|
+
print()
|
|
53
|
+
if not findings:
|
|
54
|
+
print(" (no anomalies)")
|
|
55
|
+
return 0
|
|
56
|
+
for i, f in enumerate(findings, 1):
|
|
57
|
+
print(f" [{i}/{len(findings)}] sev{f.severity} {f.rule_id}")
|
|
58
|
+
print(f" {f.summary}")
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def run_rca_coldchain() -> int:
|
|
63
|
+
"""W3 demo: detect anomalies and trace each to a root-cause decision."""
|
|
64
|
+
from rich.console import Console
|
|
65
|
+
from rich.panel import Panel
|
|
66
|
+
|
|
67
|
+
console = Console()
|
|
68
|
+
events = sorted(WMSAdapter().fetch(), key=lambda e: e.parsed_timestamp)
|
|
69
|
+
findings = list(default_detector().run(events))
|
|
70
|
+
g = build_coldchain_seed()
|
|
71
|
+
|
|
72
|
+
console.print(f"[bold]madcop RCA demo[/] — {len(findings)} finding(s) on {events[0].subject_id}\n")
|
|
73
|
+
if not findings:
|
|
74
|
+
console.print(" [dim](no findings to trace)[/]")
|
|
75
|
+
return 0
|
|
76
|
+
for i, f in enumerate(findings, 1):
|
|
77
|
+
console.print(f"[cyan]━━━ finding {i}/{len(findings)} ━━━[/]")
|
|
78
|
+
console.print(f" rule: [yellow]{f.rule_id}[/]")
|
|
79
|
+
console.print(f" summary: {f.summary}")
|
|
80
|
+
chain = trace(f, g)
|
|
81
|
+
if not chain.steps:
|
|
82
|
+
console.print(" [dim](no causal chain — subject not in knowledge graph)[/]")
|
|
83
|
+
continue
|
|
84
|
+
console.print(f" chain: [bold]{len(chain.steps)}[/] step(s), root cause:")
|
|
85
|
+
console.print(Panel(explain(chain), title="root cause", border_style="red"))
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main(argv: list[str] | None = None) -> int:
|
|
90
|
+
parser = argparse.ArgumentParser(
|
|
91
|
+
prog="madcop",
|
|
92
|
+
description="madcop — the supply chain cop that goes mad for anomalies.",
|
|
93
|
+
)
|
|
94
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
95
|
+
|
|
96
|
+
# run <scenario>
|
|
97
|
+
run_p = sub.add_parser("run", help="Run a scenario")
|
|
98
|
+
run_sub = run_p.add_subparsers(dest="scenario", required=True)
|
|
99
|
+
run_sub.add_parser("coldchain", help="W1: print the cold-chain event stream")
|
|
100
|
+
run_sub.add_parser("anomalies", help="W2: run anomaly detection on the cold-chain stream")
|
|
101
|
+
run_sub.add_parser("rca", help="W3: detect anomalies and trace each to a root cause")
|
|
102
|
+
|
|
103
|
+
# demo <scenario> — alias for `run`
|
|
104
|
+
demo_p = sub.add_parser("demo", help="Alias for `run`")
|
|
105
|
+
demo_sub = demo_p.add_subparsers(dest="scenario", required=True)
|
|
106
|
+
demo_sub.add_parser("coldchain", help="W1: print the cold-chain event stream")
|
|
107
|
+
demo_sub.add_parser("anomalies", help="W2: run anomaly detection on the cold-chain stream")
|
|
108
|
+
demo_sub.add_parser("rca", help="W3: detect anomalies and trace each to a root cause")
|
|
109
|
+
|
|
110
|
+
args = parser.parse_args(argv)
|
|
111
|
+
if args.cmd in ("run", "demo"):
|
|
112
|
+
if args.scenario == "coldchain":
|
|
113
|
+
return run_coldchain()
|
|
114
|
+
if args.scenario == "anomalies":
|
|
115
|
+
return run_anomalies_coldchain()
|
|
116
|
+
if args.scenario == "rca":
|
|
117
|
+
return run_rca_coldchain()
|
|
118
|
+
parser.print_help()
|
|
119
|
+
return 2
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
sys.exit(main())
|
|
File without changes
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Adapter contract — the seam between the outside world and `UnifiedEvent`.
|
|
2
|
+
|
|
3
|
+
Every system (OMS / TMS / WMS / BMS / future) ships an adapter that implements
|
|
4
|
+
`BaseAdapter`. Two responsibilities, and only two:
|
|
5
|
+
|
|
6
|
+
1. **Pull** raw data from a system (HTTP, DB, file, etc.) and yield `UnifiedEvent`s.
|
|
7
|
+
2. **Push** actions back to the system (e.g. re-route shipment, mark exception).
|
|
8
|
+
|
|
9
|
+
We deliberately keep the adapter small. Logic that *reads* the event stream
|
|
10
|
+
lives in the anomaly engine. Logic that *acts* on decisions lives in the
|
|
11
|
+
strategy router. The adapter is just a translator.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
from typing import Iterator
|
|
18
|
+
|
|
19
|
+
from ..event import SourceSystem, UnifiedEvent
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BaseAdapter(ABC):
|
|
23
|
+
"""The contract every system adapter must satisfy.
|
|
24
|
+
|
|
25
|
+
Adapters are stateful objects (they hold a connection, an API key, a
|
|
26
|
+
file handle). They are constructed once per session and called repeatedly.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
source_system: SourceSystem # set by subclass
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def fetch(self, *, since: str | None = None, subject_id: str | None = None) -> Iterator[UnifiedEvent]:
|
|
33
|
+
"""Yield events from the upstream system, optionally filtered.
|
|
34
|
+
|
|
35
|
+
`since` is a UTC ISO 8601 string. If given, only events with
|
|
36
|
+
`timestamp >= since` should be returned.
|
|
37
|
+
|
|
38
|
+
`subject_id`, if given, restricts to events about a single business
|
|
39
|
+
object (an order, a SKU, a shipment, a contract). Adapters that
|
|
40
|
+
cannot filter server-side should filter in-memory after fetching.
|
|
41
|
+
|
|
42
|
+
Yields events in **any order** — the LangGraph orchestrator sorts
|
|
43
|
+
by timestamp.
|
|
44
|
+
"""
|
|
45
|
+
raise NotImplementedError
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def execute(self, action: "Action") -> dict:
|
|
49
|
+
"""Push an action back to the system. Returns the system's response.
|
|
50
|
+
|
|
51
|
+
See `Action` for the schema. Adapters that cannot perform an action
|
|
52
|
+
(e.g. WMS can't change a contract) should raise
|
|
53
|
+
`UnsupportedActionError`.
|
|
54
|
+
"""
|
|
55
|
+
raise NotImplementedError
|
|
56
|
+
|
|
57
|
+
def health_check(self) -> bool:
|
|
58
|
+
"""Optional. Default: assume healthy. Override for real wire checks."""
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class UnsupportedActionError(NotImplementedError):
|
|
63
|
+
"""Raised when an adapter is asked to perform an action outside its scope."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
from dataclasses import dataclass
|
|
67
|
+
from typing import Any, Mapping
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class Action:
|
|
72
|
+
"""A command the strategy router wants executed against an adapter.
|
|
73
|
+
|
|
74
|
+
The action is **adapter-agnostic**: the router does not know which system
|
|
75
|
+
will run it. The router picks an adapter based on `target_system` and the
|
|
76
|
+
adapter translates the rest.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
target_system: SourceSystem
|
|
80
|
+
action_type: str # free-form, adapter-defined vocabulary
|
|
81
|
+
subject_id: str # what the action is about
|
|
82
|
+
parameters: Mapping[str, Any] # adapter-specific args
|